// Slides · Sheets · Apps Script

Create a slide for each row in Google Slides.

Use Apps Script to duplicate a template slide for every row in a spreadsheet, swap {{placeholders}} with replaceAllText, and delete the template when done — no shape-by-shape code required.

I have a spreadsheet with 40 rows of data and I need a separate Google Slide for each one without building every slide by hand.

The script

copy · paste · trigger
createSlidesFromRows.gs
Apps Script
// Duplicate template slide once per row, swap {{placeholders}}, delete template
function createSlidesFromRows() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Data');
  var rows = sheet.getDataRange().getValues();
  var headers = rows[0];
  var pres = SlidesApp.openById('YOUR_PRESENTATION_ID');
  var slides = pres.getSlides();
  var template = slides[0]; // first slide is the template

  for (var i = 1; i < rows.length; i++) {
    var dupe = template.duplicate();
    for (var j = 0; j < headers.length; j++) {
      var tag = '{{' + headers[j] + '}}';
      dupe.replaceAllText(tag, String(rows[i][j]));
    }
  }

  template.remove(); // clean up the template slide
}

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

Walkthrough

Design your template slide first, then write zero layout code

Open your Google Slides deck and style slide 1 exactly the way you want every output slide to look — fonts, colors, logo, background, the works. Anywhere you want dynamic data, type a placeholder in double curly braces: {{Name}}, {{Title}}, {{Revenue}}. The exact string inside the braces must match your spreadsheet column header, case-for-case.

That one slide is the only design work you do. The script calls template.duplicate() in a loop, which copies every shape, image, and style perfectly. replaceAllText then walks every text box on the duplicate and swaps {{Name}} for the actual cell value. You never call addTextBox or setFontSize. I keep a notes column in my sheet that I deliberately leave out of the template so it never shows up on slides — just don't add a {{Notes}} placeholder and the column is ignored.

Put your spreadsheet data on a sheet named Data with column headers in row 1. The script reads headers = rows[0] and constructs the tag string from each header name, so adding a new column to your sheet automatically wires up a new placeholder — no code change required.

What replaceAllText actually replaces (and what it misses)

replaceAllText on a Slide object replaces text inside every Shape and Table cell on that slide. It does not touch image alt-text, speaker notes, or text baked into a chart. If you need notes populated too, call dupe.getNotesPage().replaceAllText(tag, value) inside the same inner loop.

One gotcha: the match is case-sensitive. {{name}} and {{Name}} are different strings. The first time I hit this I spent 20 minutes wondering why half my placeholders weren't swapping — the header row had mixed casing from a CSV export. Normalize everything to Title Case in the sheet or lowercase in both places; pick one and be consistent.

Numbers and dates come back from getValues() as JavaScript numbers and Date objects. String(rows[i][j]) handles numbers fine. For dates, you probably want Utilities.formatDate(rows[i][j], Session.getScriptTimeZone(), 'MMM d, yyyy') instead of String() or you will get a raw timestamp.

Running the script and what to authorize

Paste the script into Apps Script (Extensions > Apps Script from either the Sheet or the Slides deck), replace YOUR_PRESENTATION_ID with the ID from your Slides URL (the long alphanumeric string between /d/ and /edit), and run createSlidesFromRows.

On first run Google will ask for two OAuth scopes: spreadsheets.readonly and presentations. Both are required. If you bound the script to the Sheet, SpreadsheetApp.getActiveSpreadsheet() resolves automatically; if you created a standalone script, pass the spreadsheet ID to SpreadsheetApp.openById() the same way the presentation is opened.

For decks over roughly 200 slides, Apps Script's 6-minute execution limit becomes real. The fix is to batch in chunks of 50 and use CacheService or PropertiesService to store a cursor row between runs, then trigger the function on a time-based trigger every few minutes until the cursor reaches the last row.

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 my template slide stay in the deck after the script runs?
template.remove() fires after the loop, so if the script errors mid-loop the remove never executes. Check the Apps Script execution log for the error, fix it, delete the orphaned template slide by hand, and rerun. Wrapping the loop in a try/finally block and calling template.remove() in the finally clause prevents this.
Can I use a slide in the middle of the deck as the template instead of slide 0?
Yes. slides[0] is just an index. Use pres.getSlideById('SLIDE_ID') if you want to target a specific slide by its stable ID (visible in the URL when you click the slide in the filmstrip), or pass any index to pres.getSlides()[n]. Just make sure the loop that generates duplicates still calls template.duplicate() on that same reference.
replaceAllText is not replacing my placeholder — what is wrong?
Three common causes: (1) the placeholder in the slide has a smart quote or a curly apostrophe instead of a straight brace, which Slides sometimes auto-corrects when you type; delete and retype the braces. (2) The header string in your sheet has a leading or trailing space — trim it with headers[j].trim(). (3) The text box has mixed font formatting mid-word, which splits the text into multiple runs internally; retype the placeholder from scratch in a consistent style.
How do I add images per row, not just text?
replaceAllText only handles text. For per-row images, use dupe.getShapes() to find a shape by its alt-text (set it in Slides via Format > Alt text), then call shape.replaceWithImage(imageUrl) where imageUrl is a public URL stored in your sheet. The shape's size and position are preserved, so your template layout still controls the framing.
// one good script a week

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