appscript.dev
Automation Intermediate Docs

Auto-number figures, tables, and captions

Keep cross-references consistent in long Northwind Docs — Figure 1, Table 1, automatic.

Published Dec 14, 2025

Northwind’s longer documents — proposals, technical reports, research write-ups — are full of figures and tables, and every one carries a caption. The trouble starts when someone inserts a new figure halfway through: every caption after it is now off by one, and Google Docs has no built-in caption numbering to fix it. Renumbering by hand is slow and easy to get wrong, so captions drift out of sync and cross-references stop matching.

This script walks the document body, finds every paragraph that starts with Figure or Table, and renumbers them in document order while keeping the caption text intact. Authors just write Figure: a caption here — or leave a stale number in place — and the script makes the sequence correct again on every run.

What you’ll need

  • A Google Doc whose captions each sit in their own paragraph and begin with the word Figure or Table.
  • The document ID, taken from its URL.
  • Captions written so the descriptive text follows the label, optionally with a number and colon — Figure 3: Sales by region and Figure: Sales by region are both accepted.

The script

// Matches a caption paragraph: "Figure" / "Table", an optional number,
// an optional colon, then the caption text in capture group 1.
const FIGURE_PATTERN = /^Figure\s+\d*:?\s*(.+)/;
const TABLE_PATTERN = /^Table\s+\d*:?\s*(.+)/;

/**
 * Renumbers every figure and table caption in a Google Doc so they run
 * 1, 2, 3, ... in document order. Caption text is preserved.
 *
 * @param {string} docId  The ID of the Google Doc to renumber.
 */
function renumberCaptions(docId) {
  const body = DocumentApp.openById(docId).getBody();

  // Running counters for the two caption types.
  let fig = 0;
  let tbl = 0;

  // 1. Walk every top-level element in the document body.
  for (let i = 0; i < body.getNumChildren(); i++) {
    const child = body.getChild(i);

    // 2. Only paragraphs can be captions — skip tables, images, lists.
    if (child.getType() !== DocumentApp.ElementType.PARAGRAPH) continue;

    const paragraph = child.asParagraph();
    const text = paragraph.getText();

    // 3. Test for a figure caption first, then a table caption.
    const figMatch = text.match(FIGURE_PATTERN);
    const tblMatch = text.match(TABLE_PATTERN);

    if (figMatch) {
      // Increment the figure counter and rewrite the caption.
      fig++;
      paragraph.setText('Figure ' + fig + ': ' + figMatch[1]);
    } else if (tblMatch) {
      // Increment the table counter and rewrite the caption.
      tbl++;
      paragraph.setText('Table ' + tbl + ': ' + tblMatch[1]);
    }
  }

  Logger.log('Renumbered ' + fig + ' figures and ' + tbl + ' tables.');
}

How it works

  1. renumberCaptions opens the document by ID and gets its body, then keeps two counters — fig and tbl — both starting at zero.
  2. It loops over every top-level child of the body in order.
  3. Anything that is not a paragraph (an inline image’s wrapper, a table, a list item container) is skipped, because only a paragraph can hold a caption.
  4. Each paragraph’s text is tested against FIGURE_PATTERN and TABLE_PATTERN. Both patterns accept an optional existing number and an optional colon, and capture the descriptive text into group 1.
  5. On a figure match, fig is incremented and the paragraph is rewritten as Figure N: <caption>. On a table match, tbl is incremented and the paragraph is rewritten the same way.
  6. Because the loop runs top to bottom, the numbers always follow reading order, no matter what number the author originally typed.

Example run

A report has captions that have drifted after an edit:

BeforeAfter
Figure 1: Revenue by quarterFigure 1: Revenue by quarter
Figure 1: Headcount growthFigure 2: Headcount growth
Table 3: Cost breakdownTable 1: Cost breakdown
Figure: New office layoutFigure 3: New office layout

The descriptive text never changes — only the labels are corrected, and the last caption picks up a number it never had.

Run it

This is an on-demand cleanup, run when a document is ready for review:

  1. In the Apps Script editor, open renumberCaptions and replace the docId argument with your document’s ID, or call it from another function.
  2. Click Run and approve the authorisation prompt the first time.
  3. Re-open the Doc to see the corrected sequence.

To let writers run it from the document itself, add a menu:

function onOpen() {
  DocumentApp.getUi()
    .createMenu('Document tools')
    .addItem('Renumber captions', 'renumberActiveDoc')
    .addToUi();
}

function renumberActiveDoc() {
  renumberCaptions(DocumentApp.getActiveDocument().getId());
}

Watch out for

  • The script only recognises captions that start with Figure or Table. A caption like See Figure 2 below will not be touched — which is correct for cross-references, but means a real caption that does not lead with the label is skipped.
  • Cross-references in the body text are not updated. If a sentence says “as shown in Figure 4”, that number is plain text and the script cannot find it. Renumbering can therefore desync references — keep them general or update them by hand.
  • setText rewrites the whole paragraph, which strips any inline formatting (bold, italics, links) inside the caption. Keep captions as plain text, or switch to editing only the leading label if you need rich formatting.
  • Captions inside table cells are not visited — the loop only walks top-level paragraphs. Move such captions into their own paragraph above or below the table.

Related