Dwa sposoby w skrócie
- Sposób 1: formularz → Apps Script. Formularz wysyła dane prosto do małego skryptu w Twoim arkuszu. Nic więcej nie trzeba. Dostajesz wiersze, ale bez powiadomień, bez filtra spamu poza tym, co sam napiszesz, a formularz wymaga JavaScriptu.
- Sposób 2: formularz → backend formularzy → Apps Script. Formularz trafia do backendu. Ten odsiewa spam, wysyła Ci powiadomienie, a potem przekazuje każde prawdziwe zgłoszenie do skryptu webhookiem.
Jeśli potrzebujesz tylko listy, zacznij od sposobu 1. Jeśli ktoś musi szybko odpowiadać na zgłoszenia, wybierz sposób 2.
Sposób 1: formularz wysyła dane do Apps Script
Nazwy menu podajemy tak, jak w angielskim interfejsie Google. W polskim są przetłumaczone, ale stoją w tych samych miejscach.
- Utwórz arkusz, nazwij kartę
Zgłoszeniai wpisz w 1. wierszu: Data, Imię, E-mail, Wiadomość. - Otwórz Extensions → Apps Script, usuń przykładowy kod i wklej skrypt poniżej.
- Kliknij Deploy → New deployment, wybierz Web app. W Execute as ustaw Me, w Who has access Anyone.
- Zatwierdź uprawnienia i skopiuj adres aplikacji. Kończy się na
/exec. - Wstaw ten adres do kodu formularza z następnego bloku i opublikuj stronę.
Skrypt pomija zgłoszenia z wypełnionym polem-pułapką. Zakłada też blokadę, więc dwa zgłoszenia w tej samej chwili się nie nadpiszą. Funkcja clean() nie pozwala, by wartość typu =IMPORTXML(…) zadziałała jak formuła.
// W arkuszu: Extensions > Apps Script. W 1. wierszu karty "Zgłoszenia" są nazwy kolumn.
function doPost(e) {
const p = e.parameter;
if (p.botcheck) return json({ ok: true }); // pole-pułapka: boty je wypełniają, ludzie go nie widzą
const lock = LockService.getScriptLock();
lock.waitLock(20000); // jeden zapis naraz, żeby wiersze się nie nadpisywały
try {
SpreadsheetApp.getActive().getSheetByName("Zgłoszenia").appendRow([
new Date(), clean(p.name), clean(p.email), clean(p.message),
]);
} finally {
lock.releaseLock();
}
return json({ ok: true });
}
// Wartość zaczynająca się od = + - lub @ zadziałałaby jak formuła. Apostrof zostawia ją jako tekst.
function clean(value) {
const text = String(value || "");
return /^[=+\-@]/.test(text) ? "'" + text : text;
}
function json(data) {
return ContentService.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}Formularz HTML, który wysyła dane do aplikacji
<form id="lead-form">
<label>Imię <input type="text" name="name" required autocomplete="name"></label>
<label>E-mail <input type="email" name="email" required autocomplete="email"></label>
<label>Wiadomość <textarea name="message" required></textarea></label>
<div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
<input type="text" name="botcheck" tabindex="-1" autocomplete="off">
</div>
<button type="submit">Wyślij</button>
<p id="lead-status" role="status"></p>
</form>
<script>
const form = document.getElementById("lead-form");
const status = document.getElementById("lead-status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
status.textContent = "Wysyłanie…";
try {
// Treść w formacie formularza to prosty request, więc przeglądarka nie wysyła preflight.
const response = await fetch("https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec", {
method: "POST",
body: new URLSearchParams(new FormData(form)),
});
if (!response.ok) throw new Error(String(response.status));
form.reset();
status.textContent = "Dziękujemy, wiadomość dotarła.";
} catch {
status.textContent = "Nie udało się wysłać. Spróbuj ponownie albo napisz do nas e-mail.";
}
});
</script>Czego nie robi sposób 1
- Brak powiadomień. Wiersze pojawiają się po cichu. Żeby zobaczyć nowe zgłoszenie, trzeba otworzyć arkusz.
- Słaba ochrona przed spamem. Adres jest publiczny, więc każdy, kto go znajdzie, może coś wysłać. Pole-pułapka zatrzymuje tylko proste boty.
- Wymaga JavaScriptu. Formularz używa
fetch, bo zwykłe wysłanie zostawiłoby odwiedzającego na stronie Google. - Zmiany to nowa wersja. Po edycji skryptu otwórz Manage deployments, edytuj wdrożenie i wybierz nową wersję. Adres zostaje ten sam.
- Limity. Google ogranicza Apps Script na użytkownika, np. 30 równoczesnych uruchomień i 6 minut na jedno (stan na 05.10.2026). Mała strona tego nie odczuje.
Sposób 2: webhook z backendu formularzy do arkusza
Tu formularz to zwykły formularz HTML, który wysyła dane do Formgong. Działa bez JavaScriptu i pokazuje stronę z podziękowaniem. Formgong odsiewa spam, wysyła zgłoszenie e-mailem lub na Telegram, a potem przekazuje JSON do skryptu. Plan Free obejmuje jeden webhook na formularz.
- Utwórz arkusz i kartę
Zgłoszeniaz kolumnami: ID, Data, Imię, E-mail, Wiadomość. - Wklej skrypt webhooka poniżej, zmień
SECRETi wdróż go jako aplikację, jak w sposobie 1. - W Formgong otwórz ustawienia formularza i dodaj webhook. Podaj adres aplikacji z dopiskiem
?key=i swoim sekretem. - Kliknij test webhooka w panelu, a potem wyślij prawdziwe zgłoszenie ze strony.
Apps Script dla webhooka Formgong
// URL webhooka w Formgong: adres aplikacji internetowej + "?key=" + ten sam SECRET.
// Apps Script nie widzi nagłówków żądania, więc klucz w adresie zastępuje sprawdzanie podpisu.
const SECRET = "change-me";
function doPost(e) {
if (e.parameter.key !== SECRET) return reply();
const body = JSON.parse(e.postData.contents);
if (body.event !== "submission.created") return reply(); // test z panelu wysyła zdarzenie "webhook.test"
const lock = LockService.getScriptLock();
lock.waitLock(20000);
try {
const sheet = SpreadsheetApp.getActive().getSheetByName("Zgłoszenia");
const id = body.submission.id;
// Ponowna próba ma to samo id: pomijamy, jeśli wiersz już jest.
if (!sheet.getRange("A:A").createTextFinder(id).matchEntireCell(true).findNext()) {
const f = body.submission.fields;
sheet.appendRow([id, body.submission.created_at, clean(f.name), clean(f.email), clean(f.message)]);
}
} finally {
lock.releaseLock();
}
return reply();
}
function clean(value) {
const text = String(value || "");
return /^[=+\-@]/.test(text) ? "'" + text : text;
}
// HtmlService, nie ContentService: ContentService odpowiada przekierowaniem,
// a nadawca webhooków, który nie idzie za przekierowaniami, uznaje to za błąd.
function reply() {
return HtmlService.createHtmlOutput("ok");
}Pułapka z przekierowaniem, która tworzy duplikaty
Google opisuje, że skrypt odpowiadający przez ContentService zwraca przekierowanie na jednorazowy adres w script.googleusercontent.com. Przeglądarka za nim idzie, więc w sposobie 1 wszystko gra. Nadawca webhooków, który nie idzie za przekierowaniami, widzi jednak błąd, choć wiersz już się zapisał.
Formgong ze względów bezpieczeństwa nie idzie za przekierowaniami webhooków. Traktuje je jako nieudaną dostawę i próbuje ponownie później, do pięciu prób. Każda próba uruchamia skrypt, więc jedno zgłoszenie może pojawić się pięć razy. Skrypt webhooka zapobiega temu na dwa sposoby:
- Odpowiada przez
HtmlService. Programiści zgłaszają, że taka odpowiedź wraca od razu, bez przekierowania. - Zapisuje ID zgłoszenia w kolumnie A i pomija ID, które już tam są. Nawet przy ponownej próbie wiersz będzie jeden.
Po konfiguracji otwórz dziennik webhooków w panelu. Prawdziwe zgłoszenie powinno mieć status 200 przy pierwszej próbie.
Bez kodu: Make, n8n albo Zapier
Nie chcesz ruszać Apps Script? Skieruj webhook do narzędzia automatyzacji i dodaj tam krok Google Sheets. Mamy instrukcje dla Make i n8n na własnym serwerze. Zapier też się nada, ale jego wyzwalacz webhooków wymaga płatnego planu Zapier. Darmowe opcje to więc Make albo n8n.
Format JSON i sprawdzanie podpisu na własnym serwerze opisuje dokumentacja webhooków.
Pytania i odpowiedzi
Czy formularz HTML może zapisywać do Google Sheets bez kodu?
Bezpośrednio nie. Potrzebujesz małego Apps Script albo pośrednika, np. backendu formularzy z Make lub n8n. Skrypt z tego artykułu to około 20 linii do skopiowania.
Dlaczego po wysłaniu otwiera się strona Google?
Formularz wysyła dane prosto do aplikacji, a przeglądarka pokazuje jej odpowiedź. Wysyłaj formularz przez fetch, jak w sposobie 1, albo przez backend formularzy, który przekieruje na Twoją stronę z podziękowaniem.
Skąd duplikaty wierszy z webhooka?
Nadawca zobaczył błąd i spróbował ponownie. W Apps Script ContentService odpowiada przekierowaniem, które część nadawców uznaje za błąd. Odpowiadaj przez HtmlService i pomijaj ID zgłoszeń, które już są w arkuszu.
Czy dostęp „Anyone” do aplikacji jest bezpieczny?
Każdy z adresem może wysłać dane, ale nikt nie odczyta przez niego arkusza. Trzymaj skrypt prosty, sprawdzaj tajny klucz dla webhooków i nigdy nie zwracaj danych z arkusza w odpowiedzi.
Źródła i dokumentacja
Oficjalne materiały pomocnicze: Apps Script: web apps, Apps Script: Content service redirects, Apps Script: quotas, Apps Script: Lock service, Stack Overflow: HtmlService avoids the redirect, Zapier pricing. Webhooki Formgong, Przechowywanie danych.
Artykuł w formacie MarkdownPrzeczytaj także
- Formularz w Lovable: od przycisku do prawdziwego zgłoszenia
- Formularz HTML na statycznej stronie, bez własnego serwera
- Zgłoszenia ze strony w Telegramie: czat prywatny lub grupa
- Formularz pokazuje sukces, ale email nie dociera — co sprawdzić
- Leady ze strony w Telegramie: statusy i przypomnienia zamiast CRM
- Backend dla formularzy z Lovable, Bolt, v0 i Cursor
- Bot Telegram do formularza: pisać własnego?
- Backend formularzy a RODO: 7 kryteriów wyboru
- Formularz mailto czy backend formularzy?
- Limit formularzy Webflow: co zrobić po 50
- Turnstile, reCAPTCHA czy hCaptcha do formularza
- Contact Form 7 i Elementor do Telegrama
- Formularz Squarespace nie wysyła maili?
- Formularz kontaktowy Shopify: gdzie trafia?
- Formularz Google do Telegrama: za darmo
- Formularz Wix nie wysyła maili? Rozwiązania
- Alternatywy Formspree z danymi w UE / RODO
- Formularz kontaktowy, który nic nie wysyła: test 793 stron