Apps Script example · 9 min read

Send Email on Form Submit: Copy-Paste Apps Script Pattern

Working send email on form submit example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

GmailonFormSubmit trigger

Every Google Form is one onFormSubmit trigger away from sending a real confirmation email instead of leaving submitters wondering whether their response went through. This example wires MailApp.sendEmail directly into the moment a response row lands, using the fields the respondent actually typed.

Teams that run intake forms - event RSVPs, support tickets, vendor applications - usually bolt email confirmations on later, after someone complains they never heard back. Building the trigger from day one means the acknowledgment goes out automatically, with no dashboard to check and no human in the loop.

The code below reads e.namedValues from the form submit event, falls back gracefully when a field is missing, and writes a status note back into the response sheet so you can see at a glance which rows were emailed and which were skipped.

By the end you'll have a working onFormSubmitSendEmail function, an installed trigger, and a repeatable pattern for turning any form field into part of a dynamic email body.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

Column/FieldLocationPurpose
Full NameForm field / namedValues keyUsed to personalize the email greeting
Email AddressForm field / namedValues keyRequired - recipient for MailApp.sendEmail
DepartmentForm field / namedValues keyDrives the subject line and routing message
Status columnLast column of the responses sheetWritten by the script after sending or skipping

What it does

onFormSubmitSendEmail runs automatically whenever the linked form receives a response. It pulls the respondent's name, email, and department straight out of the event object, builds a subject and body around those values, and sends the confirmation before the form's owner has even opened the spreadsheet.

  • Reads e.namedValues instead of re-reading the whole sheet
  • Skips sending only when the email field is truly empty
  • Writes a timestamped status note into the last column of the same row

Prerequisites

This only works if the script is bound to the spreadsheet that collects the form's responses, since e.range and e.namedValues are populated by the form submit event, not by editing cells manually.

  • A Google Form linked to a response spreadsheet
  • Form question titles matching 'Full Name', 'Email Address', 'Department' exactly
  • An installed onFormSubmit trigger pointing at onFormSubmitSendEmail
  • Gmail send quota available on the script's owner account

Walkthrough

The function starts by pulling namedValues off the event, which is a map of question title to an array of answers - hence the [0] on each lookup. It then guards on a missing email before doing any work, because MailApp.sendEmail will throw if the recipient is blank.

Once the guard passes, the subject and body strings are assembled with plain concatenation, kept deliberately simple so anyone editing the form's questions later can find and update the matching field name without touching MailApp syntax.

Edge cases

Form owners frequently rename questions after the form has already collected responses, which silently breaks the namedValues lookup for that field.

  • Renamed form question breaks the corresponding namedValues key - the fallback text 'Unknown' or 'General' appears instead of failing loudly
  • Multiple-choice 'other' answers can return unexpected array shapes
  • A respondent submitting twice in quick succession can trigger two separate emails since each submission is a distinct event

Testing

Submit the live form with a valid test email at least twice: once with every field filled in, and once leaving the email field blank to confirm the skip path writes 'Missing email - not sent' instead of throwing an unhandled exception.

  • Check the Apps Script executions log for the trigger run
  • Confirm the confirmation email actually lands in the test inbox, not just the log
  • Verify the status column updates on the correct row, matching e.range.getRow()

Hardening

Because MailApp quotas are shared across every script owned by the same account, a spike in form submissions can silently exhaust the daily send limit.

  • Wrap MailApp.sendEmail in a try/catch and log failures to a dedicated column
  • Switch to GmailApp.sendEmail if you need a reply-to address or CC list
  • Add a duplicate-submission check using the respondent's email plus a short time window

Variations

The same event object supports far richer emails than a status update - you can branch the entire subject and body on the Department value, or attach a PDF built from the same row.

  • Send a different template per department using a lookup object instead of one hard-coded string
  • CC a manager pulled from a separate 'Routing' sheet keyed on department
  • Combine this pattern with the email-pdf-attachment example to send a filled receipt

Full code: onFormSubmitSendEmail()

The function trusts the form submit event object for both the data (namedValues) and the target row (e.range), which keeps it fast because it never has to re-scan the sheet to find which row just changed.

function onFormSubmitSendEmail(e) {
  var namedValues = e.namedValues;
  var responseSheet = e.range.getSheet();
  var timestamp = new Date();

  var applicantName = namedValues['Full Name'] ? namedValues['Full Name'][0] : 'Unknown';
  var applicantEmail = namedValues['Email Address'] ? namedValues['Email Address'][0] : '';
  var department = namedValues['Department'] ? namedValues['Department'][0] : 'General';

  if (!applicantEmail) {
    responseSheet.getRange(e.range.getRow(), responseSheet.getLastColumn() + 1).setValue('Missing email - not sent');
    return;
  }

  var subject = 'We received your ' + department + ' request';
  var body = 'Hi ' + applicantName + ',\n\n' +
    'Thanks for submitting the form on ' + timestamp.toDateString() + '.\n' +
    'Your request has been routed to the ' + department + ' team and someone will follow up within two business days.\n\n' +
    'Regards,\nAutomated Intake System';

  MailApp.sendEmail({
    to: applicantEmail,
    subject: subject,
    body: body,
    name: 'Form Intake Bot'
  });

  var statusColumn = responseSheet.getLastColumn() + 1;
  responseSheet.getRange(e.range.getRow(), statusColumn).setValue('Confirmation sent ' + timestamp.toLocaleTimeString());
}
  1. Line 2: Captures the namedValues map so every form field can be looked up by its question title.
  2. Line 3: Resolves the exact sheet the new response landed on via e.range.getSheet().
  3. Line 6: Reads the Full Name field with a fallback of 'Unknown' if the question was renamed or left blank.
  4. Line 10: Bails out before calling MailApp if no email address was captured, avoiding a runtime exception.
  5. Line 15: Builds a subject line dynamically from the Department answer instead of a static string.
  6. Line 21: Sends the confirmation using MailApp.sendEmail's object signature so a custom sender name can be set.
  7. Line 28: Writes a timestamped confirmation note into the column right after the last existing column.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you deploy this trigger

  • 1Form question titles match the namedValues keys exactly
  • 2onFormSubmit trigger is installed on the correct spreadsheet, not the form itself
  • 3Test submission with a blank email confirms the skip path works
  • 4MailApp daily quota checked against expected submission volume
  • 5Status column doesn't collide with an existing data column
  • 6Confirmation email tested in both Gmail and a non-Gmail inbox for spam filtering

Frequently asked questions

namedValues maps each form question to its answer regardless of column order, so the script keeps working even if someone reorders or inserts a new question in the form later.

Yes - onFormSubmit fires the same way whether the form is filled out directly or embedded, since the trigger is tied to the response spreadsheet, not the form's URL.

Apps Script queues form-submit executions, so each response still gets its own event object and its own row; you won't lose a submission, though emails may go out a second or two apart.

Yes, add an attachments array to the MailApp.sendEmail options object; just be aware attachments count against the same daily MailApp quota as the emails themselves.

It will, but file upload answers come back as Drive file IDs inside namedValues rather than plain text, so you'd need to fetch the file with DriveApp before referencing it in the email body.

Add a hidden form field or a specific test email domain check near the top of the function and return early before MailApp.sendEmail runs.

Open the Apps Script project's Executions tab and filter by the onFormSubmitSendEmail function; failed runs show the exact line and error message without needing to add temporary Logger.log calls.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.