Google Docs has no mail merge button, so filling a Doc from Google Sheets rows means picking a workaround. For a batch you start yourself, with one document per row, the free script below does the job. When each document needs a table of items, has to come back signed, or has to run without anyone watching, use a document tool.
Here's the usual situation. You've got 40 clients in a sheet, a letter or invoice template in Google Docs, and a Friday afternoon you'd rather not spend copying and pasting. This guide gives you the script we'd use for that, the trap that leaves {{Price ($)}} unfilled with no error, and the point where a script stops being worth it.
Does Google have a built-in mail merge?
Only for email. Gmail on the web can send a mail merge from a Google Sheet on Workspace Business Standard and higher plans and some Education and nonprofit plans, up to 1,500 recipients a day. Google Docs has nothing like it for documents, which is why the answers in Google's own help forum point to an add-on or a script.
Set up the sheet and the template first
The script and the by-hand way both start from the same two files, a sheet and a template. Set them up like this:
- One row per document. Column names go in row 1 of the first tab, and each row under them becomes one document.
- A column that names each file. The script uses column A, so put the client or invoice name there.
- Placeholders that match the column names exactly. If the column is called
Client name, the template says{{Client name}}. Same spelling, same spaces, same capital letters. - Placeholders in the body of the document. The script fills the main body only, so a placeholder in the header or footer stays as it is.
- An empty column called Status. The script writes "Done" there after each row, so a second run skips the rows it already finished.
The free way: a short Apps Script
Apps Script is the code editor built into Google Sheets. It costs nothing and needs no add-on.
1. Open the editor from your sheet
In the sheet, click Extensions > Apps Script. Delete the empty function you see and paste this:
// Makes one Google Doc and one PDF per row on the first tab of this spreadsheet.
// Row 1 holds the column names, and the template uses {{Column name}} placeholders.
const TEMPLATE_ID = 'paste-the-template-doc-id';
const FOLDER_ID = 'paste-the-output-folder-id';
function mailMerge() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
const [headers, ...rows] = sheet.getDataRange().getDisplayValues();
const statusColumn = headers.indexOf('Status');
if (statusColumn === -1) throw new Error('Add a column named Status to the sheet.');
const template = DriveApp.getFileById(TEMPLATE_ID);
const folder = DriveApp.getFolderById(FOLDER_ID);
rows.forEach((row, index) => {
if (!row[0] || row[statusColumn] === 'Done') return; // skip blank and finished rows
const copy = template.makeCopy(row[0], folder); // column A names the file
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
headers.forEach((header, i) => fillPlaceholder(body, header, row[i]));
doc.saveAndClose();
folder.createFile(copy.getAs(MimeType.PDF));
sheet.getRange(index + 2, statusColumn + 1).setValue('Done');
SpreadsheetApp.flush(); // save "Done" now, in case the run is cut off
});
}
// Finds each {{header}} and swaps in the value as plain text.
function fillPlaceholder(body, header, value) {
const pattern = '\\{\\{' + escapeForRegex(header) + '\\}\\}';
let found;
while ((found = body.findText(pattern))) {
const text = found.getElement().asText();
const start = found.isPartial() ? found.getStartOffset() : 0;
const end = found.isPartial() ? found.getEndOffsetInclusive() : text.getText().length - 1;
text.deleteText(start, end);
if (value) text.insertText(start, value);
}
}
// findText reads the pattern as a regular expression, so characters
// like ( ) $ . ? in a column name must be escaped or nothing matches.
function escapeForRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
2. Paste in the two IDs
The template's ID is the long string in its address between /d/ and /edit. The folder's ID is everything after /folders/ when you open the Drive folder where the documents should land. Put them in place of the two placeholder values at the top.
3. Run it
Pick mailMerge in the function menu and click Run. The first time, Google asks for permission: click Review permissions and pick your account. Unless the script's owner and everyone who runs it are on the same Google Workspace domain, Google then warns "This app isn't verified", because the script hasn't been through Google's app review. It's your own copy of a script you can read above, so click Advanced, then Go to (your project name) (unsafe), then Allow.
Read that permission screen before you allow it. Google asks to "see, edit, create, and delete all of your Google Drive files", not only the template and the folder, because DriveApp, the part of Apps Script that copies files, can't be limited to one folder. After that, every row that isn't marked "Done" gets a Google Doc and a PDF in your folder.
Why it's written this way
Column names with symbols. Apps Script finds placeholders with a regular expression, a search pattern where characters like (, $ and ? have special meanings. So a column called Price ($) is read as a pattern, matches nothing, and the placeholder sits in the finished document with no error to tell you why. The escapeForRegex function at the bottom fixes that.
Values go in as plain text. The usual shortcut is replaceText, which swaps by pattern. Google's reference doesn't say what it does with a $ in the value you put in, and prices are full of them. This version finds each placeholder, deletes it and inserts the cell's text with insertText, so "$1,200.00" goes in exactly as written.
Dates and prices as you see them. The script reads what each cell shows, with your date and currency formats applied, so you get "10/31/2026" and "$1,200.00" rather than a raw date value or 1200.
Where the script hits its limits
Google stops any single run after 6 minutes, and its quota page lists "Documents created" at 250 a day on a personal account (1,500 on Workspace). On a big sheet that means running it again, and it carries on from the first row that isn't marked "Done". If a run is cut off halfway through a row, that one row is made again on the next run. Each row becomes its own file: the script doesn't put every letter into one Doc.
You can also put it on a schedule. In the editor, open Triggers (the alarm clock icon), add a time-driven trigger for mailMerge and set it to every 10 minutes. A run can't last longer than 6 minutes, so two scheduled runs never overlap, but don't click Run yourself while one is going. Don't use an edit trigger for this: it only runs when a person edits the sheet, so rows that Zapier, Make or another script add through the API are missed. Scheduled runs share a daily limit of 90 minutes on a personal Google account and 6 hours on Workspace.
We wouldn't put this script on a timer for invoices, though. When a scheduled run fails, nothing appears on your screen. Google sends a "Summary of failures" email instead, and the fix is yours to find, in code nobody else looks after.
Invoices and contracts are where this script stops being enough. An item table, a signature, a signed copy filed next to the row: each takes more code than the whole script above.
By hand, for a handful of documents
For five documents, skip the code. Open the template, click File > Make a copy, name the copy, then use Edit > Find and replace to swap each placeholder for that row's value. It's slow and easy to get wrong past ten or so, which is usually when people go looking for a guide like this one.
Add-ons such as Autocrat
Mail merge add-ons from the Google Workspace Marketplace run inside Sheets or Docs. Take Autocrat, by New Visions Cloudlab. Its Marketplace listing describes a document merge tool that creates PDFs or Google Docs from spreadsheet data, with merge tags written <<like this>>. It can attach the documents to emails and run when a new form response comes in. It's free: New Visions Cloudlab makes free tools for educators. We put the two side by side on our Autocrat comparison page.
When the merge becomes a process: DocsAutomator
DocsAutomator works from the same kind of sheet and a Google Docs template with double-brace placeholders, written {{like_this}}. You map each placeholder to a column, so the names don't have to match your headers. What changes is everything around the document: when it's made, where it goes and who signs it.
Setting it up
- Pick your Google Docs template, then the spreadsheet and sheet. It needs the header row, just like the script.
- Pick the column that names each document and the column where the PDF link should go.
- Map each placeholder to a column. The AI suggests a mapping, and you check it.
- Make test documents. Testing is free with unlimited previews and no credit card; test documents carry a watermark.
How it runs
You can start it three ways, and use more than one:
- On new rows. DocsAutomator checks the sheet every 5 minutes and makes documents for up to 50 new rows each time. It remembers the last row it handled and only looks at the rows after it. If the checks fail five times in a row, say because the sheet was deleted, it turns itself off and emails the workspace owners.
- From a link in a cell. A formula puts a link in each row. Clicking it asks "Generate this document?" and makes that row's document.
- From a menu in the sheet. A short Apps Script adds a menu that sends all or selected rows into a queue. Our docs recommend this for batches of hundreds or thousands.
Each document is saved as a Google Doc, a PDF or both in the Drive folder you choose, including subfolders named from columns, such as Clients/{{client_name}}. The PDF link lands back in the row. From the same automation you can email the PDF, or send it for e-signature or one-click acceptance with the signer's name and email taken from columns. When the last person signs, a link to the signed PDF goes into the same row, so the sheet shows what's been signed. Signing is included in every plan. Our Google Sheets to PDF page goes through the setup in more detail.
Several rows in one document (line items)
This is the case most mail merge guides skip. With the script or by hand, you'd type each item into the document yourself. Take an invoice. Invoice INV-1042 for Acme Ltd is one row on your main tab, but it has three items: design, development and hosting. Three items don't fit in one row, so they go on a second tab, called Items, each with INV-1042 in an invoice number column.
In DocsAutomator, you pick the Items tab as the line item sheet and the invoice number as the column that matches items to their invoice. In the template, add a table. In the first cell of its item row, type {{line_items_1}}{{description}}, with no space between them, then put {{quantity}} and {{price}} in the next cells. The row needs no closing placeholder. Each item becomes one row of the table, so INV-1042 comes out as one invoice with three lines. The same setup works for quotes, timesheet entries and product lists, and our line items page shows more examples.
Which method fits your merge
| Method | Cost | Runs on new rows by itself | Several rows in one document | Sent for signature |
|---|---|---|---|---|
| By hand | Free | No | Only by typing them in | No |
| The script above | Free, within Google's limits | On a schedule you add | Not without more code | No |
| Autocrat | Free | When a new form response comes in | Not on its listing | Not on its listing |
| DocsAutomator | From $19 a month, billed yearly, for 100 documents | Checks every 5 minutes | Yes, line items | Yes, included |
A sheet that makes 500 documents a month needs the Business plan: $59 a month, billed yearly, for 1,000 documents.
If you make a batch now and then, use the script. If a document needs several rows, has to come back signed, or you don't want to keep a script running, try the same template in DocsAutomator with free test documents and see what the first one looks like.




