response-ops

Google Apps Script for Google Forms: when a script is worth writing

September 29, 2026 ・ Halict Editorial

A form is collecting submissions, a spreadsheet is filling up, and somebody on the team is copying rows into email drafts. That is the moment Google Apps Script comes up. It is a reasonable answer: a few dozen lines can send a tailored confirmation, stamp a reference number, or notify whoever owns that kind of request. It is also the moment to decide how much of the intake process should live inside a script, because scripts are cheap to write and expensive to inherit. What follows is the shape of the problem: what a script genuinely adds, the documented limits it runs inside, the ways it fails silently, and the point where the script stops being the right layer.

What a script adds that the form settings do not

Google Forms already sends a confirmation message on the page, already writes responses to a spreadsheet, and already emails the form owner when a new response arrives. Anything beyond those three is where Apps Script starts.

The Forms service is the entry point. FormApp opens or creates a form, FormResponse represents one submission, and ItemResponse represents one answer inside it. From there a script can read the answers, set the confirmation message, stop accepting responses, change the response destination, and add or modify items. The full class list is in the Forms service reference.

Four jobs account for most scripts written against a form.

Routing. Read one answer, decide who owns the submission, and email that person instead of everyone. This is the single most common reason to write a script, because Forms notifies the owner and nobody else.

Tailored replies. The built-in confirmation is one block of text shown to everyone. A script can send an email whose contents depend on what was answered: a different set of instructions per department, a reference number, a copy of what was submitted.

Writing elsewhere. Copy the submission into a tracking sheet, a calendar event, a document generated from a template, or an external system through UrlFetchApp.

Guarding the data. Reject a duplicate, normalise a phone number, look up an employee ID against another sheet, or close the form once a cap is reached.

What a script does not add is a place for a human to work. It moves data. It does not give a submission an owner, a status, or a record of whether anyone replied.

The trigger is the whole design

Almost every form script hangs off one installable trigger: form submit. There are two versions of it, one bound to the form and one bound to the spreadsheet the form writes to, and they do not behave the same way. The form-bound trigger receives the form response object. The sheet-bound trigger receives the row of values. If the goal is to read answers by question title, bind to the form. If the goal is to work with the row as it will be stored, bind to the sheet.

Two properties of installable triggers matter more than the code inside them.

The first: a trigger always runs under the account of the person who created it, not the person who submitted the form. Every quota consumed, every email sent, every file touched is charged to and attributed to that account. When the person who set it up leaves the organisation, the trigger leaves with them.

The second: installable triggers can call services that require authorisation. That is why they exist, and it is also why the authorisation prompt appears once and then never again. The prompt does not reappear when the script later starts sending email to a thousand recipients.

Because a form submit trigger fires once per submission, the script has to be written for one record at a time, and it has to tolerate being run twice on the same record. Submitters double-tap. Networks retry. A script that appends a row without checking for an existing reference number will produce duplicates, and the duplicates will be discovered by whoever is counting responses at the end of the month.

The quotas that decide whether the script holds

Apps Script runs inside published limits, and the limits differ between a consumer account and a Google Workspace account. These are the figures that decide whether a script scales past a pilot.

Limit Consumer account Google Workspace
Script runtime per execution 6 min 6 min
Triggers total runtime 90 min / day 6 hr / day
Email recipients per day 100 / day 1,500 / day
URL Fetch calls 20,000 / day 100,000 / day
Simultaneous executions 30 per user 30 per user

The full table is in the Apps Script quotas and limits documentation.

Two rows deserve attention before any code is written. Email recipients per day is the limit most intake scripts hit first. A script that sends a confirmation to the submitter and a notification to two internal addresses uses three recipients per submission. On a consumer account that is roughly 33 submissions in a day. On Workspace it is 500. A registration form promoted once by email can exceed either number in an afternoon.

Total trigger runtime is the second. Ninety minutes a day sounds generous until a script fetches an external API on every submission and each call waits two seconds for a response.

The six-minute ceiling per execution is rarely a problem for one submission, and reliably a problem for the batch script somebody writes later to reprocess the whole sheet.

How form scripts fail without anyone noticing

The documented behaviour here is the important part: when a trigger fails, no error message appears on screen. Apps Script emails a failure notification to the account that owns the trigger, with links to deactivate or reconfigure it, and the frequency of those notifications can be changed.

Three consequences follow.

The failure lands in one inbox. It goes to the person who created the trigger, filtered into whatever folder their rules put it in. The team watching the spreadsheet sees nothing.

A submission that failed looks exactly like a submission that succeeded. The row is in the sheet, because Forms wrote it, not the script. Only the script's part is missing: no confirmation email, no reference number, no routing. The submitter waits, then follows up, and the follow-up is the first signal anyone gets.

Quota exhaustion presents as silence. Once the daily recipient limit is reached, the sends stop. Nothing on the form changes. Nothing in the sheet changes.

The practical countermeasure is to make success visible rather than failure. Write a timestamp or a status value into a column from inside the script, at the end of the work. A blank cell in that column is then a submission the script never finished, and it can be counted at a glance. Pair that with a daily count so that zero rows processed is distinguishable from a quiet day.

Writing it so the next person can keep it

A form script is almost always inherited before it is rewritten. Four habits decide whether the person who inherits it can work on it.

Read answers by question title, not by column position. A script that reaches for the fourth element of an array breaks the day somebody inserts a question above it, and it breaks quietly: the code still runs, it just uses the wrong value. Reading by title fails loudly instead, which is the better failure.

Put the configuration at the top. Addresses, department names, reply text and the spreadsheet identifier belong in named constants in the first twenty lines. When a department is renamed, the change should be a one-line edit that does not require reading the logic.

Make the script idempotent. Before doing the work, check whether this submission already has a reference number or a processed timestamp. If it does, stop. This one guard removes the entire class of duplicate-confirmation problems caused by resubmission and retries.

Record what was sent, not just that something happened. A line in the execution log disappears; a value written into the sheet stays next to the record it describes. The reference number, the address the confirmation went to, and the time are worth three columns.

There is a fifth habit that is organisational rather than technical: the trigger should not be owned by the person least likely to still be in the role next year. Because triggers run as their creator and failure notices go only to that account, an intake process running on a personal trigger has a single point of failure that no code review will surface. A shared account, or a documented handover step, is the fix. Anything that matters for more than a quarter deserves a note somewhere about who owns the trigger and what happens when the notifications start arriving. Common questions about running intake this way are collected under questions people ask.

Where the script stops being the right layer

A script is the right layer while the work is transformation: take this answer, decide that, write it there. It stops being the right layer when the work becomes a conversation.

The signals are consistent. Somebody asks which submissions have been answered, and the answer requires opening the sent folder. Two people reply to the same request. A column called Status appears in the sheet, then a column called Notes, then Notes 2. Someone starts a second sheet to track the first one. A submitter replies to the automated confirmation and that reply goes nowhere, because the script sent the message and no inbox is watching the address.

None of these are script bugs. They are the shape of a spreadsheet being used as a queue. A row is good at holding facts and poor at holding a thread, and every workaround makes the row wider.

What the work is Where it belongs
Send a tailored confirmation per submission Apps Script
Route a notification by department Apps Script
Validate or deduplicate on arrival Apps Script
Assign an owner to each submission Response management
Track whether a reply was sent Response management
Keep the reply history with the record Response management

The cost of finding this out late is not the script. It is that the intake process now depends on code one person wrote, with quotas charged to that person's account, and a failure channel that only that person reads. A form tool that keeps owner and status on each response covers the second column without code. The workflows this shape applies to are set out under use cases, and what one screen can hold is in features.

What to change first

Add one status column that the script itself writes at the end of its run, and count the blanks tomorrow. That single change turns silent failures into a number, and it will tell within a week whether the script is carrying the intake process or hiding a queue. If it is a queue, the next step is to move the owner and the reply history off the spreadsheet: Halict shows what that looks like in a running form, and the cost of it is on pricing.

Q1. Does a form submit trigger run as the person who submitted the form?

No. An installable trigger always runs under the account of the person who created it. All quotas, sent email and file access are charged to that account, which is also the only account that receives failure notifications. When that person leaves, the trigger stops working.

Q2. How many confirmation emails can a form script send per day?

Apps Script allows 100 email recipients per day on a consumer account and 1,500 per day on Google Workspace. Recipients are counted, not messages, so a script that emails the submitter plus two internal addresses uses three per submission.

Q3. Why did the script stop running without any error?

Trigger failures produce no on-screen error. Apps Script sends a failure notification by email to the account that created the trigger, and that message is easy to miss or filter. Reaching a daily quota looks the same as a quiet day, which is why a status column written by the script is worth more than a log.

Q4. Is it better to bind the trigger to the form or to the spreadsheet?

Bind to the form when the code needs the response object and answers read by question title. Bind to the spreadsheet when the code works with the stored row of values. The two triggers receive different event objects, so a script written for one will not work unchanged on the other.

Q5. When does a script stop being enough for handling form submissions?

When the questions shift from what the data is to who is handling it. Asking which submissions have been answered, discovering two people replied to the same request, or adding a second sheet to track the first are all signs that the submissions need an owner, a status and a reply history rather than more code.

All guides