// Docs · Drive · Apps Script

Generate a doc from a template with placeholders in Google Docs.

Use Apps Script to copy a Google Doc template, replace every placeholder with real data, and export the result — without ever touching the original template file.

I want to fill a Google Doc template with data from a script without destroying the original template each time I run it.

The script

copy · paste · trigger
generateFromTemplate.gs
Apps Script
// generateFromTemplate.gs
// Copy a template doc, fill placeholders, save.
// Replace TEMPLATE_ID with your actual file ID.

const TEMPLATE_ID = '1ABCxyz_your_template_id_here';
const OUTPUT_FOLDER_ID = '1DEFabc_your_folder_id_here';

function generateDoc(data) {
  const templateFile = DriveApp.getFileById(TEMPLATE_ID);
  const folder = DriveApp.getFolderById(OUTPUT_FOLDER_ID);
  const copy = templateFile.makeCopy(data.outputName, folder);

  const doc = DocumentApp.openById(copy.getId());
  const body = doc.getBody();

  body.replaceText('\\{\\{name\\}}', data.name);
  body.replaceText('\\{\\{date\\}}', data.date);
  body.replaceText('\\{\\{amount\\}}', data.amount);

  doc.saveAndClose();
  return copy.getUrl();
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Copy first, never edit the original

The entire operation hinges on one line: templateFile.makeCopy(). Every run produces a fresh copy in your output folder; the template stays untouched. The first time I skipped this step I spent twenty minutes figuring out why my placeholders had already been replaced when I opened the template — the previous run had eaten them.

Pass a destination folder as the second argument to makeCopy. Without it, the copy lands in Drive root, and after ten runs you have ten unlabeled files with no obvious home. DriveApp.getFolderById() takes the folder ID from the URL when you have that folder open in Drive (the long alphanumeric string after /folders/).

The file ID for your template comes from the same place: open the doc in Docs, copy the ID segment from the URL between /d/ and /edit. Hard-code it as a constant at the top of the script rather than looking it up at runtime — one less API call per invocation.

replaceText takes a regex pattern, not a plain string

Body.replaceText() is documented as taking a search pattern and a replacement. The search pattern is a Java regular expression, not a literal string. Curly braces are special characters in regex, so {{name}} in your template must be escaped as \\{\\{name\\}} in the script string — four backslashes in source because JavaScript itself eats two of them before passing the string to the regex engine.

I have watched this bite people who write replaceText('{{name}}', value) and wonder why nothing gets replaced. The call does not throw; it silently matches zero occurrences. If a replacement does not appear in the output, log body.getText() and confirm the pattern is what you expect.

One practical choice: use a delimiter that has no regex meaning. Double angle brackets like <<name>> need no escaping at all — replaceText('<<name>>', value) works literally. The tradeoff is that <<>> looks less like a template placeholder than {{}} to human editors of the template file.

saveAndClose before you do anything with the file

DocumentApp.openById() opens a live editing session. Replacements you make through getBody().replaceText() are buffered in that session. Until you call doc.saveAndClose(), the file on Drive has not received the changes — if you export to PDF or send the URL to someone before closing, they get the unmodified copy.

saveAndClose() is synchronous: the line after it runs only after the write is committed. There is no need to poll or sleep. Return the URL from copy.getUrl() after saveAndClose() and you can hand that URL to a mail merge, a form response handler, or a Sheets column immediately.

If you need a PDF instead of a Google Doc, call DriveApp.getFileById(copy.getId()).getAs('application/pdf') after saveAndClose(). That triggers a server-side conversion; the returned Blob can be attached to a GmailApp.sendEmail() call or saved back to Drive with folder.createFile(blob).

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Why does replaceText not replace anything, even though the placeholder is in the doc?
Almost always a regex escaping issue. Curly braces are special in Java regex. Either escape them as \\{\\{name\\}} or switch to a delimiter with no regex meaning, like <<name>> or [name].
Can I replace text inside a table or header, or only in the body?
getBody() covers the main body only. For headers and footers call doc.getHeader().replaceText() and doc.getFooter().replaceText() separately. Tables inside the body are covered by getBody() — replaceText recurses into table cells.
My copy ends up in Drive root instead of my target folder — what is wrong?
makeCopy() with only one argument (the name) drops the file in root. Pass the folder as the second argument: templateFile.makeCopy(name, DriveApp.getFolderById(OUTPUT_FOLDER_ID)).
How do I export the finished doc as a PDF automatically?
Call doc.saveAndClose() first, then DriveApp.getFileById(copy.getId()).getAs('application/pdf') to get a Blob. Attach it to an email with GmailApp.sendEmail() or save it with folder.createFile(blob). Do not call getAs() before saveAndClose() or you will export the pre-replacement version.