Populate speaker notes from a script sheet
Fill notes per slide from a text column — for Northwind's rehearsed all-hands decks.
Published Sep 7, 2025
Northwind’s all-hands deck has a problem familiar to anyone who has rehearsed a big talk: the speaker notes live in three places at once. The CEO drafts the script in a Google Doc, the design team builds the slides in parallel, and by the time anyone tries to paste the notes back in the deck has shifted and the numbers no longer line up. Last-minute edits to the script mean clicking into every slide’s notes pane one at a time.
This script keeps the source of truth in a sheet. You write the script in one tab — slide number on the left, notes on the right — and the script copies each row into the matching slide’s speaker notes in one pass. Edit the sheet, run the job again, and the deck catches up. It is a small piece of plumbing that turns “rehearsing the deck” into “editing the sheet”.
What you’ll need
- A
Scriptsheet with two columns:slide(1-based slide number) andnotes(the speaker notes for that slide). A header row in row 1. - The Google Slides deck the notes belong to. The script writes to its speaker-notes pane, not to the slide bodies.
- Nothing else — both files just need to be editable by you.
The script
// Column indexes in the Script sheet, 0-based. Edit if your sheet differs.
const COL_SLIDE = 0;
const COL_NOTES = 1;
/**
* Reads a two-column sheet of (slide, notes) rows and writes each row's
* text into the matching slide's speaker notes.
*
* @param {string} deckId The Slides file ID.
* @param {string} scriptSheetId The Spreadsheet holding the script.
*/
function populateSpeakerNotes(deckId, scriptSheetId) {
if (!deckId || !scriptSheetId) {
throw new Error('populateSpeakerNotes needs both a deckId and a scriptSheetId.');
}
// 1. Read the script sheet, drop the header row, ignore blank rows.
const [, ...rows] = SpreadsheetApp.openById(scriptSheetId)
.getSheets()[0]
.getDataRange()
.getValues();
const deck = SlidesApp.openById(deckId);
const slides = deck.getSlides();
let written = 0;
let skipped = 0;
for (const row of rows) {
const slideNumber = row[COL_SLIDE];
const notes = row[COL_NOTES];
// 2. Guard against blank slide numbers — common when editors leave
// empty rows between scenes for readability.
if (!slideNumber) continue;
// 3. Slide numbers are 1-based in the sheet, 0-based in the array.
const slide = slides[slideNumber - 1];
if (!slide) {
Logger.log(`Slide ${slideNumber} doesn't exist in the deck — skipped.`);
skipped++;
continue;
}
// 4. Notes live on a separate "notes page", not on the slide itself.
// The speaker-notes shape is a single text box on that page.
slide.getNotesPage()
.getSpeakerNotesShape()
.getText()
.setText(notes || '');
written++;
}
Logger.log(`Wrote notes to ${written} slides; skipped ${skipped} missing slides.`);
}
/**
* Convenience wrapper so you can click Run in the editor without writing a
* caller. Replace the IDs with your own.
*/
function populateNorthwindNotes() {
populateSpeakerNotes('1abcDeckId', '1abcScriptSheetId');
}
How it works
populateSpeakerNotesguards against missing IDs first, then reads the script sheet’s data range and drops the header row with array destructuring.- It opens the deck and grabs its slides in order. The array is 0-based but the sheet’s slide numbers are 1-based, so the script subtracts one when indexing.
- For each row it skips blank
slidecells — editors often leave empty rows between scenes for readability, and those should not blow up the loop. - If the slide number points past the end of the deck, the script logs the skipped row and carries on. That happens after slides get deleted; you’d rather see “skipped” in the log than crash mid-deck.
- Speaker notes don’t live on the slide itself — they live on a separate
“notes page” that every slide owns. The script grabs the speaker-notes
shape, then its text, and calls
setTextwith the notes (or an empty string, so blank cells visibly clear stale notes). - At the end it logs a count of writes and skips, so you can confirm the numbers match expectations before walking on stage.
Example run
Say the Script sheet looks like this:
| slide | notes |
|---|---|
| 1 | Welcome everyone — keep this to 30 seconds. |
| 2 | Three numbers to anchor the year: revenue, headcount, NPS. |
| 3 | Hand off to design — Maya is on stage from here. |
| 5 | Engineering update — start with the platform migration. |
| 7 | Closing slide. Take questions for 10 minutes. |
After a run, slides 1, 2, 3, 5, and 7 have their speaker notes populated.
Slide 4 is left as-is — if you want it blank, add a row with an empty
notes cell rather than omitting the row.
Run it
This is an on-demand job — you run it whenever the script changes:
- In the Apps Script editor, select
populateNorthwindNotesand click Run. - Approve the authorisation prompt the first time.
- Open the deck and check the speaker-notes pane on a couple of slides.
For a high-stakes talk, give the presenter a one-click trigger by adding a custom menu to the script sheet — see the extracting deck text recipe for the inverse direction.
Watch out for
- Blank cells clear notes. That is the point — re-running the script keeps
the deck in sync — but if a slide already has hand-written notes you want
to keep, leave it out of the sheet entirely rather than including it with
a blank
notescell. - The script does not reorder. If you reorder slides in the deck, the sheet’s slide numbers no longer match. Either re-export the order first, or identify slides by object ID instead of position.
- Notes pages must exist. Every slide has one by default, but a corrupted or
imported slide can be missing the speaker-notes shape — wrap the
setTextcall in a try/catch if you hit this on legacy decks. - Long notes are fine but unscrollable. Slides’ notes pane scrolls inside the shape — there is no hard limit, but blocks longer than a few paragraphs are awkward to read at a podium.
Related
Extract all deck text into a sheet
Pull text out of every slide for review, translation, or copy-editing.
Updated Jan 4, 2026
Generate sales-enablement decks per segment
Tailor Northwind's messaging slides by audience segment — fintech, healthcare, retail.
Updated Dec 28, 2025
Insert chapter divider slides from an outline
Add section-break slides between chapters in a Northwind deck.
Updated Dec 21, 2025
Build a deck accessibility checker
Flag missing alt text, low contrast, and tiny fonts across a Northwind deck.
Updated Dec 14, 2025
Drive menu and price-list signage from a Sheet
Generate display slides for a Northwind venue — menus or price lists driven by a Sheet.
Updated Dec 7, 2025