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
FigureorTable. - 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 regionandFigure: Sales by regionare 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
renumberCaptionsopens the document by ID and gets its body, then keeps two counters —figandtbl— both starting at zero.- It loops over every top-level child of the body in order.
- 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.
- Each paragraph’s text is tested against
FIGURE_PATTERNandTABLE_PATTERN. Both patterns accept an optional existing number and an optional colon, and capture the descriptive text into group 1. - On a figure match,
figis incremented and the paragraph is rewritten asFigure N: <caption>. On a table match,tblis incremented and the paragraph is rewritten the same way. - 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:
| Before | After |
|---|---|
Figure 1: Revenue by quarter | Figure 1: Revenue by quarter |
Figure 1: Headcount growth | Figure 2: Headcount growth |
Table 3: Cost breakdown | Table 1: Cost breakdown |
Figure: New office layout | Figure 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:
- In the Apps Script editor, open
renumberCaptionsand replace thedocIdargument with your document’s ID, or call it from another function. - Click Run and approve the authorisation prompt the first time.
- 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
FigureorTable. A caption likeSee Figure 2 belowwill 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.
setTextrewrites 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
Generate personalized study guides from notes
Reformat raw notes into structured study guides — for Northwind's internal training programme.
Updated Feb 8, 2026
Build a contract-clause assembly system
Construct Northwind agreements from a library of approved clauses — drag-drop in code.
Updated Feb 1, 2026
Translate and resolve Doc comments
Localise reviewer feedback on a shared Doc so multilingual teams can collaborate.
Updated Jan 25, 2026
Auto-archive finalized Docs to dated folders
File completed Northwind Docs by month so the active folder stays focused on in-flight work.
Updated Jan 18, 2026
Build a fillable intake form inside a Doc
Create structured intake forms with placeholder fields readers can fill — for client briefs.
Updated Jan 11, 2026