The IR
August 27, 2026 · View on GitHub
The IR is a JSON file that describes a whole deck: which pages exist, what is on them, and which theme they wear. It is what an agent writes, and it is the only input pptwise render needs.
Run pptwise schema for the full JSON Schema. Feed it to a model before asking it to write IR.
A deck
A deck (PptxIR) carries:
version— currently"4", and the default when omitted.filename.narrative— a preset id string, or a partial axes object (see Narratives).theme— anidplus optionalstyle/brandoverrides.metaandassets.brand— logo placement.branding— where the brand footer and logo appear:"cover-only"(keep the brand logo on cover and chapter pages, drop the footer rule, meta, and logo on content and ending pages),"full"(draw the content-page footer and logo, and paint confidentiality and date on cover and ending meta rows), or"minimal"(drop the content-page footer rule and meta, keep the logo). Omitted equals"cover-only". Other postures leave confidentiality and date off the canvas even whenmetacarries them. Layoutbranding: "none"still wins. Theme motifs are unaffected. Omitted by default. Write"full"only when every content page needs the brand footer.slides— required, ordered.
Everything but slides is optional and has a sensible default.
assets is { images: { [id]: { src, alt? } } }. Components reference images by asset_id, so the same image can be reused across slides without duplication. alt, when set, lands in the exported PPTX's standard accessibility-description slot for that image — what PowerPoint's "Edit Alt Text" panel reads and writes. An image component whose asset has no alt exports exactly as before the field existed.
A deck can also carry a seed: an integer that keeps auto-selected layouts stable across revisions (see Layout selection).
A slide
Each slide has:
type—cover,chapter,content, orending.layout— an explicit page-layout id. It always wins over auto-selection. Omit it and pptwise picks one.arrangement— how a content slide's body is laid out, for exampletwo_columnorkpi_focus.components— the typed units that fill the page (bullets,kpi_cards,image,chart, …).
Any slide may also set a stable id (what spec pages and validation errors reference it by), placeholder: true (a page with no content yet — injected by assemble for a spec page nobody has filled in, skipped by content-quality checks, and blocking render unless --draft), and notes (aliases note/speaker_notes/speakerNotes), which exports as a native PowerPoint speaker note. Notes are for the presenter's own view: never drawn on the slide canvas, never counted toward layout capacity. The spoken script belongs in notes. The agent playbook's Sparse-page contract (skills/pptwise/SKILL.md) is the rule. If the file must stand alone as a document, put the extra words in notes or use a PDF.
Field names that drift
55 synonym pairs across component types — kpi title→label, quote content→text, swot strength→strengths, bmc partners→key_partners, and so on — are silently normalized to the canonical name at validate time. validate/render/preview print a note listing what changed, never a hard error.
That rescue covers weak-model synonym drift only. It does not cover pre-v4 vocabulary. A v4-labeled document that writes scenario instead of narrative, mode/delivery instead of strategy/pacing, or the old narrative/text/presentation axis values is rejected outright, with the current names and values listed. An explicit version: "3" (or "2") is rejected the same way, with a migration pointer.
Full-body components
Eight component types fill a slide's entire content rect and must be the only component on their slide:
swot— strengths/weaknesses/opportunities/threats.bmc— the nine-block Business Model Canvas.waterfall— a running-total bridge chart.gantt— dated bars on a shared numeric axis.pest— a political/economic/social/technological macro-environment scan.five_forces— Porter's competitive-forces hub-and-spoke.heatmap— a value-driven color grid.sankey— a layered, quantity-proportional flow diagram, shipped as native editable vectors rather than the rasterized image this chart type usually gets.
Mixing one in with another component fails validate instead of silently dropping the sibling.
Schema stability
The v4 IR schema is frozen as of 0.4.0. Future evolution is additive only: new optional fields, new enum members. Any breaking change ships under a new top-level version value, with the same reject-and-migrate treatment v3 got.
pptwise migrate <v3-file.json> -o <out.json> converts a v3 file to v4 deterministically — field renames, plus the v4 leftover rewrites chrome → branding, bloom → classroom, logo_wall → image_grid, and banner-heading → two-column. The sibling deck.plan.json → deck.spec.json conversion is under Deck projects.
Narratives
A narrative is three axes that set editorial discipline, independent of the theme's visual style:
strategy— how the argument is built:pyramid,storytelling,instructional,showcase,briefing.pacing— how dense the content is:dense,balanced,spacious.audience— a tone anchor:executive,technical,customer,public. No rendering effect yet.
Set the IR's top-level narrative to a named preset string ("boardroom-report") or a partial axes object ({ "pacing": "spacious" }). An omitted axis, or an omitted narrative entirely, falls back to general (briefing × balanced × public). An unknown preset name or axis value is a validate error that lists what is available.
pacing drives the content-quality gate and the body-text baseline (paragraph, bullets, and callout only — every other component's type scale and the heading system are unaffected). The per-slide component budget and the bullets budget tighten from dense toward spacious, while the body font size grows the other way. Density is additionally capped by whichever layout the slide resolves to, whichever ceiling is tighter.
| pacing | body text | components / slide | bullets |
|---|---|---|---|
dense | 24px (18pt) | 5 | up to 6 items, ~27 characters each |
balanced (the default) | 24px (18pt) | 4 | up to 5 items, ~25 characters each |
spacious | 32px | 3 | up to 4 items, ~22 characters each |
These are editorial guidance, not hard limits: validate reports them as warnings and still succeeds. Only genuine render-safety ceilings can block generation.
Body copy never shrinks below 24px (18pt). Captions, footnotes, ticks, and other secondary type never shrink below 16px (12pt). An item that still cannot fit at that floor is a hard validate error. The renderer does not paint an ellipsis.
pptwise validate reports the exact numbers that applied to each slide. pptwise narratives [--json] lists the named presets (each carrying soft theme recommendations, a suggestion rather than a constraint) plus the raw axes tables.
Layout selection
When a slide omits layout, pptwise resolves one in four deterministic steps:
- Start from the selected theme's compiled face pool for that page type. A partial custom theme inherits this boundary from its
base. A complete custom theme declares all four pools itself. The shared registry has 43 auto-selectable standard layouts and 87 pin-only standard layouts. See Themes. - Apply the rare
narrativesOnlyhard filter for the resolved strategy. - Combine strategy, optional slide
beat, and theme tendencies withMath.max, then make a seeded weighted pick. - When that pick repeats the immediately preceding page and another candidate exists, redraw deterministically without the repeated id.
An explicit layout skips those steps, except an unoffered sparse climax pin (SPARSE_LAYOUT_IDS): effectiveRequestedLayout strips it, auto-pick runs, validate warns, and ok stays true. quote-stage is pin-only but not sparse. Whether the content fits is flagged separately by validate's density gate, never by selection — so editing a page's content cannot silently flip its layout.
The pick is fully deterministic: the same IR always resolves the same way, so preview and the final render never disagree. Staying stable across revisions — editing one page without reshuffling every other page's auto-pick — additionally needs a persisted seed, resolved in this order:
- An explicit
ir.seed. Full revision stability, always wins. - A deck project's own seed.
pptwise assemblederives one from the spec's filename and page ids the first time a spec omitsseed, and prints the value — copy it intodeck.spec.json'sseedfield to persist it. - Neither set: a content hash of
filenameplus every slide'sheading. Editing any heading reshuffles every auto-picked layout deck-wide.
pptwise assemble also writes every auto-picked layout back into the assembled deck.json, leaving a page file's own explicit layout untouched. The CLI notes how many pages it filled in.
The mechanics behind all of this are in selection-and-seed.md.
Deck projects
A deck can be authored two ways, and every command that takes IR accepts either: a single IR JSON file (everything above), or a deck project directory — the same content split across files, so an agent can spec out the structure first and then write and revise page by page instead of holding one growing JSON blob in context.
my-deck/
deck.spec.json the locked spec: page order, type, and heading for every page
theme.json optional custom version 1 theme, registered automatically
pages/<page-id>.json one file per filled page (components/layout/arrangement/background/image_side/footnote/notes)
assets/ local images, auto-registered by filename (image id = filename without extension)
deck.spec.json validates on its own, before any page exists: pptwise spec validate deck.spec.json checks the schema plus the strategy-aware hard gates (boundary pages, heading length, beat rotation, page count vs. pacing).
A spec page with no matching pages/<id>.json becomes a placeholder slide — heading only, not missing — so a partially written deck always assembles and previews. pptwise render refuses to export a deck with unfilled placeholders unless you pass --draft. pptwise preview never gates on them.
A directory still carrying the pre-v4 deck.plan.json instead of deck.spec.json is not read directly. pptwise migrate <dir> -o <dir> converts it in place: it writes deck.spec.json alongside, never overwrites, never deletes the source — delete deck.plan.json yourself once you have confirmed the new file. A directory with both files present is a hard error, never a guessed priority.
pptwise assemble <dir> materializes spec + pages + assets into a single IR JSON file (deck.json by default). pptwise disassemble <ir.json> -o <dir> does the reverse, and is lossy by design: spec-only fields like beat/focus have no IR-side home to recover. render/validate/preview accept a directory directly too, assembling in memory first.
A deck project directory can be referenced by a bare name instead of a path. pptwise render my-deck -o out.pptx resolves my-deck under $PPTWISE_HOME/decks ($PPTWISE_HOME defaults to ~/.pptwise) when no local file or directory of that name exists.
Theme selection has five levels, highest first: CLI --theme, authored artifact selection (deck.spec.json for a project or theme.id for a bare IR), project pptwise.config.json, user ~/.pptwise/config.json, then the schema default consulting. Project theme.json and --theme-file register custom ids but do not select them. Both config layers can set decksDir to redirect where bare names resolve. The project value resolves against its config file, while the user value resolves against $PPTWISE_HOME.
The format's finer points are in deck-projects.md.