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 — an id plus optional style/brand overrides.
  • meta and assets.
  • 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 when meta carries them. Layout branding: "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:

  • typecover, chapter, content, or ending.
  • 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 example two_column or kpi_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 titlelabel, quote contenttext, swot strengthstrengths, bmc partnerskey_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 chromebranding, bloomclassroom, logo_wallimage_grid, and banner-headingtwo-column. The sibling deck.plan.jsondeck.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.

pacingbody textcomponents / slidebullets
dense24px (18pt)5up to 6 items, ~27 characters each
balanced (the default)24px (18pt)4up to 5 items, ~25 characters each
spacious32px3up 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:

  1. 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.
  2. Apply the rare narrativesOnly hard filter for the resolved strategy.
  3. Combine strategy, optional slide beat, and theme tendencies with Math.max, then make a seeded weighted pick.
  4. 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:

  1. An explicit ir.seed. Full revision stability, always wins.
  2. A deck project's own seed. pptwise assemble derives one from the spec's filename and page ids the first time a spec omits seed, and prints the value — copy it into deck.spec.json's seed field to persist it.
  3. Neither set: a content hash of filename plus every slide's heading. 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.