// Forms · Gmail · Apps Script

Email a summary when a form is submitted in Google Forms.

How to use an installable Apps Script trigger on the Form itself to email a formatted response summary every time someone submits — without wiring through a linked Sheet.

I want to receive an email with the submitted answers every time someone fills out my Google Form, without checking the responses tab manually.

The script

copy · paste · trigger
formSubmitEmail.gs
Apps Script
// Installable trigger: bound to the Form, not the Sheet.
// Set up via: Triggers → Add trigger → onFormSubmit (From form)
function onFormSubmit(e) {
  var form = FormApp.getActiveForm();
  var responses = e.response.getItemResponses();
  var lines = ['New response for: ' + form.getTitle(), ''];

  for (var i = 0; i < responses.length; i++) {
    var item = responses[i];
    lines.push(item.getItem().getTitle() + ': ' + item.getResponse());
  }

  var body = lines.join('\n');
  var recipient = 'you@example.com';
  var subject = 'Form response: ' + form.getTitle();

  MailApp.sendEmail(recipient, subject, body);
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

The trigger you install determines what shape your data arrives in

Google Forms can fire two different onFormSubmit triggers, and they are not interchangeable. If you open the script editor from inside the Form (Extensions → Apps Script), you get the Form-bound script container. If you open it from a linked Sheet, you are in a different container with a different event object. The Form-bound trigger delivers e.response, a FormResponse object, from which you call getItemResponses() to get an array of ItemResponse objects — one per question. The Sheet-bound trigger delivers e.values, a flat array of strings in column order, with a timestamp jammed in position zero.

That shape difference matters the moment you have a question with no answer (skipped optional field). In e.values the skipped column is an empty string, and your column-to-question mapping silently shifts if you ever reorder questions. In e.response.getItemResponses(), the skipped item is simply absent from the array, so you can always call getItem().getTitle() to know which question you are looking at. For a summary email, the Form-bound path is less fragile.

The first time I wired this up I used a simple trigger named onFormSubmit (no installation step), and it never fired. Simple triggers in Forms do not receive the event object for form submissions — they require the installable version, which you register through the Triggers panel.

Installing the trigger correctly

In the Apps Script editor attached to your Form, open the Triggers panel (the clock icon in the left sidebar, or Edit → Current project's triggers). Click Add Trigger. Set the function to onFormSubmit, the deployment to Head, the event source to From form, and the event type to On form submit. Save it. Apps Script will ask for authorization — grant it.

You do not add a function called onFormSubmit and expect the runtime to wire it automatically. That is the simple-trigger convention, and it does not apply here. The installable trigger is a separate registration that tells Apps Script to call your function and pass the full event object, including e.response.

One trigger per function is enough. If you run the setup step more than once without deleting the old trigger, you will get duplicate emails on every submission. Check the Triggers panel before adding a new one.

Adapting the email for checkboxes and multi-select answers

getResponse() returns different types depending on the question type. For short-answer and paragraph questions it returns a string. For checkbox and multi-select questions it returns a JavaScript array. Calling toString() on the array produces a comma-separated string, which is readable in an email body, but if you want cleaner formatting you can call join(' / ') explicitly.

A date question returns a string in YYYY-MM-DD format. A time question returns a string in HH:MM format. Grid questions return a two-dimensional array. If your form uses a grid, you will want to flatten it before pushing it into the lines array — otherwise [object Array] lands in the email.

I keep a small helper that detects arrays with Array.isArray() and joins them, then passes everything else straight through. That covers 90% of form question types without needing to branch on item.getItem().getType().

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Why is my onFormSubmit function never called even though I saved the script?
You have a simple trigger, not an installable one. Simple triggers named onFormSubmit do not fire for form submissions — they only fire for Sheets events. Go to the Triggers panel (the clock icon), click Add Trigger, set the event source to From form and the event type to On form submit, and save. Delete any duplicate triggers from previous attempts.
My trigger fires but e.response is undefined. Why?
The script is attached to the Sheet, not the Form. A Sheet-bound onFormSubmit trigger receives e.values and e.namedValues, not e.response. Open the Form, go to Extensions → Apps Script, and create the trigger there. The Form-bound container is a separate script project from the Sheet-bound one.
Can I send the email to whoever submitted the form instead of a fixed address?
Only if you collected their email in the form. If you turned on Collect email addresses in the form settings, e.response.getRespondentEmail() returns it as a string. Otherwise you have no reliable way to get it — Apps Script does not expose the submitter's Google account to the trigger by default.
How do I avoid hitting the MailApp daily sending quota?
MailApp is capped at 100 emails per day for personal Google accounts and 1,500 for Workspace accounts. If your form gets more submissions than that, switch to GmailApp.sendEmail(), which shares the same quota but lets you use aliases, or batch submissions into a digest using a time-based trigger instead of a per-submission trigger.