Generating Slide Decks From a Script
From the slaide language deck, rendered with slaide.

Generating Slide Decks From a Script

The decks worth automating are the ones nobody wants to build by hand: the same ten slides every Monday with new numbers, a customer deck that’s 90% template and 10% their logo and their data, a CI job that should produce a status deck the same way it produces a test report.

None of that is possible when the deck format needs a GUI to write. It’s completely ordinary once the deck is a text file, because a text file is just a string, and generating a string from data is a problem every language already solves.

The basic pattern

A .slaide file is Markdown for the words and a YAML block for the theme. Templating it is string substitution, nothing more:

import subprocess
from string import Template

TEMPLATE = Template("""theme:
  palette: ink
  font: IBM Plex Sans

# Weekly report - $week
MRR: $$$mrr, up $growth% week over week

# What shipped
$changes
""")

deck = TEMPLATE.substitute(
    week="Aug 25",
    mrr="18,400",
    growth="3.1",
    changes="- New onboarding flow\n- Trial length cut to 7 days",
)

with open("weekly.slaide", "w") as f:
    f.write(deck)

subprocess.run(["slaide", "render", "weekly.slaide", "--out", "weekly.pdf"])

That’s the whole mechanism. Pull the numbers from wherever they live, mail-merge them into the template, render, done. No API to learn beyond the CLI, because the CLI is the API.

Pulling real numbers in

The version I actually run pulls MRR from a billing API instead of hardcoding it:

#!/usr/bin/env bash
set -euo pipefail

mrr=$(curl -s "$BILLING_API/mrr" | jq -r '.value')
growth=$(curl -s "$BILLING_API/growth" | jq -r '.pct')

sed -e "s/{{mrr}}/$mrr/" -e "s/{{growth}}/$growth/" \
  template.slaide > weekly.slaide

slaide render weekly.slaide --out weekly.pdf
slaide render weekly.slaide --out weekly.pptx

Two output formats from one render step, because sometimes the report goes into an email as a PDF and sometimes someone wants to edit a slide before a meeting, and the .pptx export is a real editable file so that request doesn’t bounce back to me.

Customer decks from a template

Same idea, one more layer. A sales deck with six slides of fixed pitch and two slides that need the customer’s name, logo path, and their specific numbers plugged in. I keep the fixed slides in the template and treat the variable ones as a small data file:

customers = load_csv("pipeline.csv")
for row in customers:
    deck = TEMPLATE.substitute(
        name=row["company"],
        logo=row["logo_path"],
        savings=row["projected_savings"],
    )
    path = f"decks/{row['slug']}.slaide"
    write(path, deck)
    subprocess.run(["slaide", "render", path, "--out", f"decks/{row['slug']}.pptx"])

Twenty customer decks in the time it used to take to duplicate one PowerPoint file and hope I remembered to change every instance of the old customer’s name.

In CI

Render is a CLI command with an exit code, so it’s a normal build step. A status deck as a build artifact, checked for the render succeeding, no different from any other generated file in a pipeline:

- name: Render status deck
  run: |
    npm install -g slaide
    slaide render status.slaide --out status.pdf
- uses: actions/upload-artifact@v4
  with:
    name: status-deck
    path: status.pdf

Agents generating decks directly

The core ships an MCP server and an Agent Skill, which means a model can write the .slaide file itself rather than going through a template at all. I use this for one-off decks where there’s no recurring shape to template, just “make me a five-slide summary of this quarter’s numbers.” The agent writes the text, I look at the render, ask for a fix if a slide is too dense. It’s still ordinary text editing on the model’s end, which is the reason any of this works - a .pptx was never something a model could safely generate, and a text file always was.

The honest limit is the same one that applies everywhere with Slaide: this is good at content-heavy decks, and it stays out of your way as long as the layout stays simple. If you need a hand-designed slide in the middle of a generated deck, generate the rest and build that one separately.