Google Forms onFormSubmit Trigger Generator
Generate a Google Apps Script that installs an onFormSubmit trigger and sends every response to your destination. Slack incoming webhooks get Block Kit formatting; any other HTTPS URL gets plain JSON. Two minutes from paste to live.
Build your onFormSubmit trigger
Pick where the trigger should send each response. The script regenerates as you type.
Need email or Google Sheet output instead? Use Apps Script's MailApp.sendEmail or SpreadsheetApp in the onFormSubmit handler. See the FAQ below for snippets.
Leave blank to use the channel bound to the webhook URL.
// Google Forms onFormSubmit trigger → Slack
//
// Generated by https://routeforms.com/tools/google-forms-on-submit-trigger-generator
//
// Install (90 seconds):
// 1. In your Google Form, click the three-dot menu → Apps Script.
// 2. Replace the existing function myFunction() {} with the contents of
// this file, then save (Cmd+S).
// 3. In the function dropdown, select installOnFormSubmit and click Run.
// 4. When Google asks, click Advanced → Go to ... (unsafe) → Allow.
// You're authorising your own script — read it before running.
// 5. Submit a test response to verify end-to-end.
const DESTINATION_URL = "PASTE_YOUR_WEBHOOK_URL_HERE";
const TEST_MODE = false;
function installOnFormSubmit() {
var form = FormApp.getActiveForm();
if (!form) {
throw new Error(
"No active form. Open this script from inside the Google Form: " +
"Form → three-dot menu → Apps Script."
);
}
// Remove any existing onFormSubmit triggers so re-running install is safe.
var existing = ScriptApp.getProjectTriggers();
for (var i = 0; i < existing.length; i++) {
if (existing[i].getHandlerFunction() === "onFormSubmit") {
ScriptApp.deleteTrigger(existing[i]);
}
}
ScriptApp.newTrigger("onFormSubmit")
.forForm(form)
.onFormSubmit()
.create();
Logger.log("\u2713 onFormSubmit trigger installed for form: " + form.getTitle());
}
function collectResponse(e) {
var form = FormApp.getActiveForm();
var response = e.response;
var itemResponses = response.getItemResponses();
var data = {};
for (var i = 0; i < itemResponses.length; i++) {
var ir = itemResponses[i];
var title = ir.getItem().getTitle();
var answer = ir.getResponse();
if (Array.isArray(answer)) answer = answer.join(", ");
data[title] = answer;
}
data._meta = {
formTitle: form.getTitle(),
formId: form.getId(),
responseId: response.getId ? response.getId() : null,
submittedAt: new Date().toISOString()
};
return data;
}
function buildSlackPayload(data) {
var formTitle = (data._meta && data._meta.formTitle) || "Google Form";
var lines = [];
for (var k in data) {
if (k === "_meta" || k.charAt(0) === "_") continue;
var v = data[k];
var rendered = (v === null || v === undefined)
? "\u2014"
: (typeof v === "object" ? JSON.stringify(v) : String(v));
if (rendered.length > 500) rendered = rendered.slice(0, 500) + "\u2026";
lines.push("*" + k + ":* " + rendered);
}
return {
text: "New response from " + formTitle,
blocks: [
{ type: "section", text: { type: "mrkdwn", text: ":inbox_tray: *New form response*" } },
{ type: "section", text: { type: "mrkdwn", text: "*Form:* " + formTitle + "\n*Submitted:* " + new Date().toLocaleString() } },
{ type: "divider" },
{ type: "section", text: { type: "mrkdwn", text: lines.join("\n") || "_(no fields)_" } }
]
};
}
function onFormSubmit(e) {
try {
var data = collectResponse(e);
var payload = buildSlackPayload(data);
if (TEST_MODE) {
Logger.log("TEST MODE \u2014 would have POSTed to " + DESTINATION_URL + ":");
Logger.log(JSON.stringify(payload, null, 2));
return;
}
var res = UrlFetchApp.fetch(DESTINATION_URL, {
method: "post",
contentType: "application/json",
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
var code = res.getResponseCode();
if (code < 200 || code >= 300) {
Logger.log("Destination returned " + code + ": " + res.getContentText());
}
} catch (err) {
Logger.log("onFormSubmit error: " + err);
}
}
Paste a destination URL above to enable Copy and Download.
A correct onFormSubmit trigger, not a guess
The trigger configuration that ships in this script avoids the three mistakes every hand-rolled version makes.
- Bound to the form, not the spreadsheet.
installOnFormSubmit()callsScriptApp.newTrigger(...).forForm(form).onFormSubmit().create()— the only configuration that fires on actual form submissions. - Idempotent install.Existing onFormSubmit triggers are deleted before a new one is created. Running install twice doesn't produce two triggers (which would double-post).
- Reads from the active form.
FormApp.getActiveForm()only works when the editor is opened from inside the form. The install function throws a clear error if you ran it from script.google.com instead. - Output-aware payload builder. Slack destinations get Block Kit messages with the field list pre-formatted. Generic webhooks get a flat JSON object plus a
_metablock with formId, responseId, and submittedAt. - Non-2xx logging. If the destination rejects, the response code and body land in the Apps Script Executions log so you know what to fix.
From paste to live trigger in 90 seconds
- 1Open the Apps Script editor inside your Google FormIn your Google Form, click the three-dot menu (top-right) → Apps Script.
- 2Paste the generated script and saveDelete the placeholder
function myFunction(). Paste the generated script above. Save with⌘S. - 3Run installOnFormSubmit onceAt the top of the editor, choose
installOnFormSubmitfrom the function dropdown and click Run. - 4Authorise when Google asksClick Advanced → Go to ... (unsafe) → Allow. The script needs permission to read form responses and (if you POST) to make outbound HTTPS calls.
- 5Submit a test responseFill the form once. TEST_MODE on? Check the Apps Script Executions log. TEST_MODE off? Check your destination — Slack channel, server log, wherever.
When the trigger doesn't fire
Three things go wrong, in this order of frequency:
- Wrong event source. If you created the trigger by hand, check that event source is From form, not From spreadsheet. The spreadsheet variant fires when Apps Script writes the response row, which isn't the same hook.
- Auth flow abandoned. The first time you run
installOnFormSubmit, Google shows the unverified-script warning. If you closed the tab without clicking Allow, the trigger creation never completed. Re-run install. - Wrong account.The script editor and the form must be in the same Google account. If they're not,
FormApp.getActiveForm()returns null and install throws.
Where Apps Script triggers stop being enough
The trigger gives you a single deterministic path: form submitted → POST to one URL. The moment that path needs to branch — different leads to different channels, a retry when Slack returns 429, a delivery log a client can read — you're maintaining a Google Apps Script project nobody on the team wants to own.
RouteForms is the managed version of this trigger: same Apps Script paste, but routing rules in a dashboard, idempotent retries on the response ID, and a per-form delivery log you can audit. Free for 30 responses a month.
Frequently asked questions
What is an onFormSubmit trigger?▾
A Google Apps Script trigger that fires automatically every time someone submits the Google Form it's attached to. Apps Script hands your handler an event object (e) containing the response. This generator wires up the trigger and emits a handler that POSTs the response to Slack or to a webhook URL.
What's the difference between 'From form' and 'From spreadsheet' triggers?▾
Google Apps Script lets you bind onFormSubmit either to the form directly or to the spreadsheet it sends responses to. They fire at slightly different points and hand you different event shapes. For form-to-Slack or form-to-webhook pipelines, always pick 'From form' — that's what installOnFormSubmit() does for you, so you can't mis-pick.
Can I send to multiple destinations from one trigger?▾
Yes — extend the onFormSubmit handler. After collectResponse(e), call buildSlackPayload + UrlFetchApp.fetch for each destination URL, or branch on a field value. For real conditional routing (IF Budget > 50000 → channel A, ELSE → channel B), it's cleaner to wire the form into RouteForms and use the routing-rule UI than to hand-maintain branches in Apps Script.
Can I send an email instead of POSTing?▾
Yes — replace the UrlFetchApp.fetch call with MailApp.sendEmail. Inside onFormSubmit you'd do something like: MailApp.sendEmail({ to: 'team@example.com', subject: 'New ' + data._meta.formTitle, htmlBody: '<pre>' + JSON.stringify(data, null, 2) + '</pre>' }). The collectResponse helper still gives you the structured data; the destination is what changes.
Can I write to a Google Sheet?▾
Yes — call SpreadsheetApp.openById('<your-sheet-id>').getSheetByName('Responses').appendRow([data._meta.submittedAt, data['Name'], data['Email'], …]) inside onFormSubmit. Note: Google Forms already writes to a linked sheet for free, so build this only if you need a different sheet or a different shape.
Why does Google warn me the script is unverified?▾
Because it is — you just wrote it. Apps Script flags every unpublished script as 'unverified' so you actively review your own code before granting it permission. Click Advanced → Go to ... (unsafe) → Allow. The trigger needs permission to read form responses and (if you POST) to make outbound HTTPS calls.
What does the trigger code actually do?▾
Three things. (1) installOnFormSubmit() — wipes any old onFormSubmit triggers and installs a fresh one bound to the active form. (2) collectResponse(e) — turns the event into a flat data object plus a _meta sub-object with formId, responseId, and submittedAt. (3) onFormSubmit(e) — builds the destination-specific payload (Slack Block Kit if you picked Slack, plain JSON otherwise), POSTs it, logs non-2xx responses.
How do I verify the trigger is actually installed?▾
In the Apps Script editor, click the clock icon in the left sidebar (Triggers). You should see one row: 'onFormSubmit' / 'From form' / 'On form submit'. If it's missing, installOnFormSubmit() didn't complete — usually because you ran it from the wrong account or the auth flow was abandoned. Re-run it from inside the form.
Want this without maintaining Apps Script?
RouteForms gives you the same path with routing rules, retries, and a delivery log — free for 30 responses a month.
Keep reading
The Slack-specific generator with Block Kit detection. Same install path.
If your destination is a custom HTTPS endpoint (CRM, server, n8n), this one picks the payload shape.
Already have a trigger? Paste its config to find out whether it'll actually fire on form submit.
Decodes the cryptic errors Apps Script throws when a trigger or fetch goes wrong.
Long-form walkthrough of the full pipeline with routing rules, delivery logs, and pricing.