Generate a PDF from each form response
Turn Northwind submissions into formatted documents stored under the right client folder.
Published Jul 5, 2025
Northwind’s brief-intake form gathers all the right answers, but a row in a sheet is not what designers want to read at the start of a project. They want a one-page document — branded header, headings in the right places, an archive copy in the client’s Drive folder.
This script turns every new submission into exactly that. It copies a Google
Doc template, swaps in the answers using {{placeholders}}, and drops the
finished file in a shared output folder. The original spreadsheet row stays
put as the system of record; the doc is the thing the team actually opens.
What you’ll need
- A Google Doc template containing the placeholders
{{name}},{{date}}, and{{body}}somewhere in the body. Style it however you like — fonts, logo, page numbers — the placeholders are the only contract. - A Drive folder to receive the generated docs. Use a single shared folder, or extend the script to look up a client-specific folder by name.
- The form’s response sheet, since the trigger runs on the sheet. The form
must capture at least a
Namefield; everything else is auto-included in the body.
The script
// IDs for the template Doc and the output folder. Both come from
// the URL of the file in Drive.
const TEMPLATE = '1abcResponseTemplateId';
const OUTPUT = '1abcResponseDocsId';
/**
* Installable form-submit trigger. Copies the template, fills in the
* placeholders from the submission, and saves the result to OUTPUT.
*
* @param {GoogleAppsScript.Events.SheetsOnFormSubmit} e The submit event.
*/
function onFormSubmit(e) {
// Guard against being run by hand from the editor — without the event
// object there are no answers to copy in.
if (!e || !e.namedValues) {
Logger.log('No event payload — run this from a form-submit trigger.');
return;
}
// 1. Pull the submitter's name. Default to "anon" so a missing answer
// still produces a file rather than crashing the trigger.
const name = e.namedValues.Name?.[0] || 'anon';
const date = new Date();
const isoDate = date.toISOString().slice(0, 10);
// 2. Build the body block. One line per answer, in submission order.
// Object.entries preserves the order the form delivers, which is the
// order the questions appear on the form.
const body = Object.entries(e.namedValues)
.map(([k, v]) => k + ': ' + v[0])
.join('\n');
// 3. Copy the template into the output folder. The filename is
// sortable-date + name, so newest-by-name is one click in Drive.
const fileName = isoDate + ' — ' + name;
const copy = DriveApp.getFileById(TEMPLATE)
.makeCopy(fileName, DriveApp.getFolderById(OUTPUT));
// 4. Open the copy and swap each placeholder for the real value.
// replaceText takes a regex, but plain strings work too.
const doc = DocumentApp.openById(copy.getId());
const docBody = doc.getBody();
docBody.replaceText('{{name}}', name);
docBody.replaceText('{{date}}', isoDate);
docBody.replaceText('{{body}}', body);
// 5. Save and close so the file is fully written before the trigger
// returns. Apps Script will flush eventually, but explicit is safer.
doc.saveAndClose();
Logger.log('Generated doc: ' + fileName + ' (' + copy.getId() + ')');
}
How it works
- The trigger fires on every form submission and receives the event object,
with answers in
e.namedValues. - It pulls the submitter’s
Name(defaulting toanon) and today’s date in ISO form. The ISO date is both used inside the doc and prefixed onto the filename so files sort chronologically. - It flattens every answer into a body block — one
key: valueline per question — that drops into the{{body}}placeholder. DriveApp.makeCopyclones the template into the output folder. Keeping the original template untouched means you can edit styling without affecting past runs.- The copy is opened with
DocumentAppandreplaceTextswaps each{{placeholder}}for its real value. The placeholders can appear anywhere in the body — heading, table cell, footer. saveAndClose()flushes the changes immediately so the file is ready as soon as the trigger returns.
Example run
A form submission like this:
| Field | Value |
|---|---|
| Name | Acme Joinery |
| Project | Spring catalogue redesign |
| Deadline | 2026-06-30 |
| Brief | 12-page A4, photography supplied. |
Produces a doc named 2026-05-27 — Acme Joinery in the output folder, with
the template’s placeholders filled in:
Project brief — Acme Joinery Date: 2026-05-27
Name: Acme Joinery Project: Spring catalogue redesign Deadline: 2026-06-30 Brief: 12-page A4, photography supplied.
The original sheet row stays intact for tracking; the doc is what the team opens when the project kicks off.
Trigger it
Use the installable form-submit trigger on the response sheet. Simple triggers can’t open Docs, so this has to be installable:
- From the response sheet, open Extensions → Apps Script.
- In the editor, open Triggers (the clock icon).
- Add trigger, choose
onFormSubmit, event source From spreadsheet, event type On form submit. - Approve the authorisation prompt — it will ask for Docs and Drive scopes.
- Submit a test response and confirm the doc lands in the output folder with the placeholders replaced.
Watch out for
- Placeholders are matched as plain text.
{{Name}}and{{name}}are different toreplaceText. Keep the casing consistent between the template and the script. - The script writes only
{{name}},{{date}}and{{body}}. Leave a placeholder unset in the script and it shows up literally in the doc — add areplaceTextline for every placeholder in your template. - One doc per submission can fill a folder quickly. Either rotate the
output folder monthly, or sub-folder by client by looking up a folder by
name before the
makeCopycall. DriveApp.makeCopycounts against the daily Drive quota. A few hundred submissions a day is fine; tens of thousands needs batching or moving to a background job.- If your template uses tables,
replaceTextworks inside cells, but only on flat text — a placeholder split across runs (mid-bold, for example) won’t be replaced. Type each placeholder as plain unformatted text and style around it.
Related
Trigger an onboarding sequence on form submit
Kick off tasks when a new Northwind hire submits their starter form.
Updated Oct 17, 2025
Build a content-submission queue
Collect Northwind guest posts or ideas for review through a Form.
Updated Oct 9, 2025
Score sentiment in open-text feedback
Rate Northwind feedback comments without manual review — using the in-Sheet sentiment function.
Updated Oct 5, 2025
Build a peer-nomination and voting system
Collect and tally Northwind nominations for awards or initiatives — one ballot, anonymous.
Updated Oct 1, 2025
Roll a form over each cycle
Archive old responses and reset for the next Northwind cycle — quarterly OKR check-ins.
Updated Sep 27, 2025