Send pre-filled personalized form links
Give each Northwind client a partly completed form — saves them retyping their details.
Published Aug 26, 2025
Northwind’s quarterly check-in form asks every client for their name, email and a few short answers. The name and email are already on file, which means every client opens the form, retypes the two fields they shouldn’t have to, and then gets to the real questions in a slightly worse mood.
Pre-filled links fix this in one pass. The script reads the client roster, asks the form to build a unique URL per client with the name and email already populated, emails each one their personal link, and records the link in the sheet so it can be reused. Clients open the form, see their details in place, and answer the one or two questions that actually need answering.
What you’ll need
- A
Clientssheet with columnsname,email, andprefilledFormUrl. LeaveprefilledFormUrlempty for clients you haven’t sent yet — the script skips any row that already has one. - A Google Form with two text questions titled
Your nameandYour email. Add more pre-fillable questions by extending thenameItem/emailItempattern below. - The form ID and the clients spreadsheet ID, pasted into the constants at the top.
The script
// IDs for the form being pre-filled and the clients sheet. Both come
// from the URL of the file in Drive.
const FORM_ID = '1abcFormId';
const CLIENTS_SHEET_ID = '1abcClientsId';
// Titles of the form questions the script will pre-fill. Edit these
// to match your form exactly — including punctuation and case.
const NAME_QUESTION = 'Your name';
const EMAIL_QUESTION = 'Your email';
/**
* Walks the client roster and emails each one a form link with their
* name and email already filled in. Skips rows that already have a
* URL, so it is safe to re-run.
*/
function sendPrefilled() {
const form = FormApp.openById(FORM_ID);
// 1. Find the two questions we want to pre-fill. Throw early if
// either is missing — silently sending blank pre-fills is worse
// than a clear failure.
const items = form.getItems();
const nameItem = items.find((i) => i.getTitle() === NAME_QUESTION);
const emailItem = items.find((i) => i.getTitle() === EMAIL_QUESTION);
if (!nameItem || !emailItem) {
Logger.log('Could not find both form questions: ' + NAME_QUESTION + ', ' + EMAIL_QUESTION);
return;
}
const sheet = SpreadsheetApp.openById(CLIENTS_SHEET_ID).getSheets()[0];
const values = sheet.getDataRange().getValues();
const [h, ...rows] = values;
// 2. Header->column index. Re-orderable columns, no string-magic.
const col = Object.fromEntries(h.map((k, i) => [k, i]));
if (col.name === undefined || col.email === undefined || col.prefilledFormUrl === undefined) {
Logger.log('Clients sheet must have name, email and prefilledFormUrl columns.');
return;
}
let sent = 0;
rows.forEach((r, i) => {
// 3. Skip clients we have already sent — re-running the job won't
// spam anyone. Clear the cell to resend.
if (r[col.prefilledFormUrl]) return;
if (!r[col.email]) return;
// 4. Build a "response" without submitting it. toPrefilledUrl
// serialises the answers into the form's prefill query string.
const response = form.createResponse();
response.withItemResponse(nameItem.asTextItem().createResponse(r[col.name]));
response.withItemResponse(emailItem.asTextItem().createResponse(r[col.email]));
const url = response.toPrefilledUrl();
// 5. Record the URL in-memory so a one-time write at the end
// persists everything.
values[i + 1][col.prefilledFormUrl] = url;
// 6. Send the email. Plain body keeps it from landing in spam.
GmailApp.sendEmail(
r[col.email],
'Quick form — pre-filled for you',
'Hi ' + r[col.name] + ',\n\n' +
'Your quarterly check-in form is ready. We have already filled in ' +
'your name and email — just answer the remaining questions:\n\n' +
url + '\n\nThanks,\nNorthwind'
);
sent++;
});
// 7. Write the sheet back once at the end — cheaper than a write
// per row and keeps us under the per-execution quota.
sheet.getDataRange().setValues(values);
Logger.log('Sent ' + sent + ' pre-filled links.');
}
How it works
- The script opens the form and locates the two questions to pre-fill by title. A missing question logs and exits — better than emailing dozens of blank pre-fills.
- It reads the clients sheet in one call and builds a header->index map so the column order can be rearranged in the sheet without breaking the script.
- For each client row, it skips any that already have a
prefilledFormUrl(so the script is idempotent) or no email address (nothing to send to). form.createResponse()builds an unsaved response object;withItemResponseattaches the two answers, andtoPrefilledUrl()serialises them into the form’s prefill query string. No row is added to the response sheet — it’s URL generation only.- The URL is recorded back into the in-memory
valuesarray. A singlesetValuescall at the end persists every change. - The email body is plain text and addresses the client by name, which keeps it out of “looks like marketing” spam filters.
Example run
The Clients sheet on Monday:
| name | prefilledFormUrl | |
|---|---|---|
| Acme Joinery | [email protected] | (empty) |
| Bramble & Co | [email protected] | (empty) |
| Coastal Logistics | [email protected] | https://…&entry.1=Coastal… |
The script processes Acme and Bramble (Coastal is skipped — already sent), generates two prefill URLs, writes them into the sheet, and emails each client. The next run sends nothing new until someone clears one of those cells or a new row is added.
A pre-filled URL ends up looking like:
https://docs.google.com/forms/d/e/.../viewform?usp=pp_url&entry.123=Acme%20Joinery&entry.456=hello%40acme.test
The client clicks it, sees their name and email already in the form, and gets straight to the new questions.
Run it
This is an on-demand job — usually run once at the start of each quarterly cycle, not on a schedule:
- In the Apps Script editor, select
sendPrefilledand click Run. - Approve the authorisation prompt the first time — it needs Forms, Sheets and Gmail scopes.
- Check the execution log to confirm the send count matches the number
of empty
prefilledFormUrlrows. Send yourself a row first as a smoke test.
To send to a new batch later, add the new rows to the sheet (leaving
prefilledFormUrl empty) and run again. To resend to a specific client,
clear their prefilledFormUrl cell first.
Watch out for
- Question titles must match exactly. Rename
Your nametoFull nameon the form and the script logs an error rather than emailing blank pre-fills. - This only pre-fills text questions. Multiple-choice, checkbox, scale and
date items have their own create-response shapes — extend the
withItemResponsepattern as needed. - Pre-filled URLs are not secrets, but they are personal. Anyone with the link can submit as that client. Don’t post them publicly, and don’t include sensitive defaults like billing IDs.
GmailApp.sendEmailcounts against the daily quota (100 for free, 1500 for Workspace). For a roster bigger than that, throttle the loop with a per-day cap and pick up where you left off next run.- The “skip if already sent” rule is the URL being non-empty. If you ever edit the form’s questions, clear the column to force fresh URLs — otherwise old links pre-fill into the wrong fields.
Related
Email managers a weekly response summary
Digest the week's Northwind form submissions for managers — counts, hot topics, sample quotes.
Updated Oct 13, 2025
Send escalating reminders for missing responses
Chase Northwind teammates who haven't submitted — three reminders, increasing urgency.
Updated Sep 11, 2025
Re-send a recurring weekly check-in form
Push a status form to the Northwind team each week — no Friday reminder needed.
Updated Aug 22, 2025
Send branded confirmation emails on submission
Reply to every Northwind form submitter with a styled receipt — no more raw Google receipts.
Updated Jul 1, 2025
Build a conditional intake-form router
Send long Northwind forms only to relevant respondents based on short pre-survey answers.
Updated Sep 15, 2025