Skip to content
// Docs · Apps Script

Merge multiple docs into one in Google Docs.

How to use Apps Script to copy content from multiple Google Docs into a single destination document, handling paragraphs, tables, and list items without throwing on unknown element types.

I want to programmatically combine several Google Docs into one file without losing tables or list formatting.

The script

copy · paste · trigger
mergeDocs.gs
Apps Script
// Merges all source docs into destDocId, in order.
// Run from the Apps Script editor; no triggers needed.
function mergeDocs() {
  var sourceIds = [
    'DOC_ID_ONE',
    'DOC_ID_TWO',
    'DOC_ID_THREE'
  ];
  var dest = DocumentApp.openById('DEST_DOC_ID').getBody();
  for (var i = 0; i < sourceIds.length; i++) {
    var srcBody = DocumentApp.openById(sourceIds[i]).getBody();
    var total = srcBody.getNumChildren();
    for (var j = 0; j < total; j++) {
      var el = srcBody.getChild(j);
      var type = el.getType();
      if (type === DocumentApp.ElementType.PARAGRAPH) {
        dest.appendParagraph(el.asParagraph().copy());
      } else if (type === DocumentApp.ElementType.TABLE) {
        dest.appendTable(el.asTable().copy());
      } else if (type === DocumentApp.ElementType.LIST_ITEM) {
        dest.appendListItem(el.asListItem().copy());
      }
    }
  }
}

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

Walkthrough

Why there is no single appendElement call

The Document service in Apps Script does not expose a generic `appendElement` method on `Body`. Every element type has its own typed append method: `appendParagraph`, `appendTable`, `appendListItem`, and so on. That means a merge loop that ignores element types will throw `TypeError: el.copy is not a function` the moment it hits a table, because you would be passing an `Element` where the API expects a `Table`.

The fix is to call `getType()` on each child element and switch on `DocumentApp.ElementType`. The type enum values are strings like `PARAGRAPH`, `TABLE`, `LIST_ITEM`, `INLINE_IMAGE`, and `HORIZONTAL_RULE`. In practice the first three account for almost all real document content. The code above handles those three and silently skips anything else (images, rules) rather than crashing the entire merge.

Getting the doc IDs and running the script

The ID for any Google Doc is the long string between `/d/` and `/edit` in its URL. Paste those into the `sourceIds` array and put the destination doc's ID in `'DEST_DOC_ID'`. The destination doc must already exist; the script does not create it.

Open `script.google.com`, create a new project, paste the function, save, and click Run. The first run will trigger an OAuth consent screen asking for permission to read and modify your Docs. Accept it. Subsequent runs execute without prompting. I keep this script bound to the destination doc (via Extensions > Apps Script from within that doc) so the project stays easy to find later.

One quota note: `DocumentApp.openById` counts against the Apps Script read quota, which is 50 opens per execution for consumer accounts and higher for Workspace. For more than roughly 40 source docs, batch across multiple executions or use a time-driven trigger that processes a slice at a time.

Preserving list grouping across source documents

List items in Google Docs carry a list ID that ties them to a specific list group. When you copy a `ListItem` from one document into another, Apps Script assigns it a new list ID automatically, so items from different source docs will not accidentally merge into the same numbered list. That behavior is what you want.

Where it surprises people: if a single source doc has two separate bullet lists separated by a paragraph, and that paragraph is a heading (which Apps Script still sees as `PARAGRAPH`), the heading copies correctly and the two lists stay separate. The problem only appears if you skip paragraph elements, which drops the visual boundary and makes two distinct lists appear to run together in the output. Copy everything in child order and this does not happen.

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
Does this preserve bold, italic, and heading styles?
Yes. The `.copy()` call on a paragraph or list item carries over all inline text attributes (bold, italic, underline, font size) and the paragraph style (Heading 1, Normal, etc.). What it does not carry over is named styles you defined via a custom Style; those stay tied to the source document's stylesheet.
Why does my table appear empty after the merge?
You likely called `appendTable` with the raw `Element` object rather than casting it first with `el.asTable()`. The cast returns a proper `Table` instance whose `.copy()` includes all rows and cells. Skipping the cast passes an untyped element and the API creates an empty table shell.
Can I merge docs from a shared drive I do not own?
Yes, as long as the account running the script has at least Viewer access on each source doc. The OAuth scope `https://www.googleapis.com/auth/documents` covers cross-ownership reads. If a source doc is restricted and the script account lacks access, `openById` throws a `GoogleJsonResponseException` with a 403; fix it by sharing the source doc with the script-runner's account.
The script stops after a few docs with a 'Service invoked too many times' error.
That is the Apps Script DocumentService quota cap. For consumer (free) Google accounts, the daily limit is 250 document opens. For Google Workspace accounts it is higher but still finite. Split your source IDs into batches of 20-30 and run the function multiple times, or set a time-driven trigger that processes one batch per minute.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.