
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.