How OPF Works
An OPF document is one JSON file that answers three questions about a presentation:
- What does it say? —
slides, with content payloads and assets. - Who is it for and why? —
audience,purpose,tone,language, andnarrative. - What should it look like? —
design, resolved through themes, color schemes, and font schemes.
The document records intent; an engine (a renderer, exporter, or editor) turns that intent into pixels or .pptx output. OPF deliberately stops at the format boundary: it never embeds OOXML, layout geometry, or renderer-specific state. You — or your agent — own the story, the data, and the ask; the format's job is to keep all of that readable, diffable, and out of <p:sp> tags.
Anatomy of a document
Presentation
├── identity ...... name, description, organization, speaker, author
├── intent ........ audience, purpose, tone, language, narrative, takeaway, duration
├── content ....... slides[]
│ ├── title / subtitle / tag / notes / section / beat / layout
│ └── one content shape:
│ root payload (a single content kind)
│ blocks[] (ordered payloads, placement inferred)
│ region keys (3x3 placement grid)
├── design ........ theme, colorScheme, fontScheme, background, logo, header, footer
├── variables ..... named colors, referenced from content as "var:<id>"
├── assets ........ named media sources, referenced as "asset:<id>"
└── catalogs ...... per-kind overrides: inline records and/or custom sourcesOnly slides is required. The smallest valid document:
{
"name": "Minimal OPF Deck",
"slides": [
{ "title": "Minimal OPF Deck" },
{ "title": "Next Steps", "text": "Use this as a starting point." }
]
}Everything else in the format is optional and additive.
Slides and content
A slide carries its content in one of three shapes. Pick the loosest shape that says what you mean — engines handle placement.
1. Root payload — one content kind directly on the slide. The kind is inferred from the field present (text, items, chart, table, image, video, code, metric, quote, timeline); see content-payloads.md for the full table.
{
"title": "Operating Metric",
"metric": { "value": "42%", "label": "Review cycle reduction", "trend": "up" }
}Multiple kinds at the slide root (with no explicit type, blocks, or regions) are shorthand for the equivalent blocks:
{
"title": "Habitat",
"text": "Jaguars are strongly associated with water and dense cover.",
"items": ["Rainforests and flooded wetlands", "Large defended territories"]
}2. blocks — an ordered list of payloads when a slide has several pieces of content but placement should stay renderer-inferred:
{
"title": "Customer Feedback",
"blocks": [
{ "table": { "columns": ["Theme", "Mentions"], "rows": [["Speed", 42], ["Ease of use", 31]] } },
{ "quote": { "text": "The new workflow cut review time in half.", "attribution": "Operations Lead" } }
]
}3. Promoted region keys — a 3×3 placement grid when position matters:
left center right
+--------------------+--------------------+--------------------+
top | top:left | top:center | top:right |
+--------------------+--------------------+--------------------+
middle | middle:left | middle:center | middle:right |
+--------------------+--------------------+--------------------+
bottom | bottom:left | bottom:center | bottom:right |
+--------------------+--------------------+--------------------+
A bare column key ("left") spans all three rows.
A bare row key ("top") spans all three columns.
Keys span neighbors with "+" and intersect rows with columns via ":".The spans compose into the slide shapes you actually want:
"left" + "center+right" "top" + "middle+bottom"
(sidebar + main) (headline band + body)
+----------+------------------+ +-------------------------------+
| | | | top |
| | | +-------------------------------+
| left | center+right | | |
| | | | middle+bottom |
| | | | |
+----------+------------------+ +-------------------------------+
"top" + "middle+bottom:left" + "middle+bottom:center+right"
(headline band, then sidebar + main)
+---------------------------------------------+
| top |
+---------------+-----------------------------+
| | |
| middle+bottom | middle+bottom:center+right |
| :left | |
| | |
+---------------+-----------------------------+That last shape in JSON:
{
"title": "Adoption Doubled",
"top": { "text": "Adoption doubled while support load stayed flat." },
"middle+bottom:left": { "metric": { "value": "2.1x", "label": "Adoption" } },
"middle+bottom:center+right": {
"chart": { "type": "line", "data": { "columns": ["Month", "Teams"], "rows": [["Jan", 12], ["Feb", 18]] } }
}
}And the two-column shape from the grid above:
{
"title": "Operating Snapshot",
"left": { "table": { "columns": ["Metric", "Value"], "rows": [["Revenue", "$4.2M"]] } },
"center+right": { "chart": { "type": "line", "data": { "columns": ["Month", "Revenue"], "rows": [["Jan", 3.4]] } } }
}Region keys on one slide must not overlap, and regions cannot be mixed with a root payload. Slide-level strings title, subtitle, and tag sit alongside whichever content shape you use, and render into the matching placeholders of the resolved layout.
Layouts are hints, not contracts
Slide.layout optionally references a record in the layouts catalog. The layout's placeholders describe what the layout exposes (a title slot, chart regions, image treatment) — they do not constrain what the slide may contain. This loose coupling is intentional:
- A slide may use any region keys or payloads regardless of its declared layout. Validators do not error on a slide/layout mismatch.
- When
layoutis omitted, engines infer one from the slide's payload or region keys. - Free-form layout names that don't resolve through any catalog fall through to engine-defined layouts.
The principle, used throughout OPF: slides are the source of truth. Layouts, narratives, and design records guide rendering; they never invalidate content.
Narrative is a plan; the deck holds a pointer
A narrative is a plan: a story arc, its ordered beats (labeled segments such as hook, problem, evidence, ask) and what each beat must do. A deck is the product. So the plan lives in the narratives catalog and the deck holds only a pointer: narrative is a string, a catalog id ("classic-story", "pitch-deck"), an HTTPS URL or a pkg: reference. A beat carries its blueprint: type is the slide's Slide.type, layout is its Slide.layout, and instructions and thoughtCues guide the author. A record's duration { min, max } is the talk length in minutes it suits.
Slides link themselves to beats with Slide.beat. Nothing forces them to, and the preview and the PPTX export draw nothing from the narrative. validate warns about a slides[].beat id the narrative does not define (opf/unknown-beat) and about a root duration outside the narrative's range (opf/duration-outside-narrative), and reports a beat that no slide references (opf/unused-beat, info).
A custom narrative is a record in catalogs.narratives.records, and narrative names its id. There is one shape, the catalog record:
{
"name": "Schema Pitch",
"narrative": "technical-proof",
"duration": 12,
"catalogs": {
"narratives": {
"records": [
{
"id": "technical-proof",
"name": "Technical Proof",
"duration": { "min": 8, "max": 20 },
"beats": [
{ "id": "contract", "name": "Contract", "type": "text", "instructions": "State what stays stable." },
{ "id": "evidence", "name": "Evidence", "type": "chart" },
{ "id": "adoption", "name": "Adoption", "type": "list" }
]
}
]
}
},
"slides": [
{ "beat": "contract", "title": "The Contract", "text": "Beats describe intent without constraining slides." },
{ "beat": ["evidence", "adoption"], "title": "Proof And Ask", "items": ["One slide may cover several beats."] }
]
}The plan is also a skeleton deck: a planner emits slides that carry beat, type, layout, title and notes, which is a valid deck from the start. Deck-level concerns that are not part of the storyline (audience, tone, takeaway, duration) live as siblings on the presentation root.
Catalog references and how they resolve
Most reusable values in OPF are references into catalogs: named collections of records, each identified by a kebab-case id. The referencing fields are narrative, language, tone, audience, purpose, design.theme, design.colorScheme, design.fontScheme, Slide.layout, Chart.type, and the platform keys in socials.
Every reference resolves through the same chain, first match wins:
"design": { "colorScheme": "cool-horizon" }
|
v
1. catalogs.colorSchemes.records[] inline records in this document
| miss
v
2. catalogs.colorSchemes.source custom registry declared in this document
| miss
v
3. default catalog https://www.pptx.gallery/color-schemes
| miss (engines read the pinned snapshot in
v spec/catalogs/ and never fetch it)
validation warning — never an error — and an engine fallbackcatalogs.<kind>.source is one source or an ordered array of sources. An array is a search path: the engine consults the sources in order, the first record with the id wins, and the default catalog is appended implicitly at the end (so [a, b] behaves as [a, b, default]). Inline records[] still win over every source. Engines never fetch a source at run time: a source is resolved only from records the host supplies for it (for example the catalogSources option of opf-render and opf-pptx), or from the bundled snapshot when it is a pptx.gallery or pkg:@openpresentation/opf/... source; an unknown source contributes nothing and the lookup continues with the next entry. The validator treats any non-empty array like a string source: ids it cannot check against the bundled catalogs are not reported as unknown.
Decision, 2026-09-30 (agent decision, vetoable). The spec coverage audit found that
opf-renderthrewsource.startsWith is not a functionfor an arraysource, although the schema allows it. The array form is implemented as the search path the schema describes, with the resolution rules above, instead of being removed from the schema. The owner can veto this by narrowingCatalogEntry.sourceto a single string (a breaking change for documents that use the array form).
pptx.gallery publishes the default catalog; the copy bundled in spec/catalogs/ and the @openpresentation/opf package is a pinned snapshot of it, so resolution is deterministic offline. See the default catalog for the endpoints and the snapshot.
When a reference is omitted entirely, engines fall back to their own defaults (see spec/reference/engine-defaults.json for a reference example — that file is engine configuration, not part of the document contract).
Three reference forms are accepted wherever a catalog reference is allowed:
- Bare id for the common case:
"narrative": "classic-story". - Object form for catalog-backed overrides:
{ "id": "cool-horizon", "accent1": "#0F4C81" }resolves the record as a base, then inline fields win per key. - URL or
pkg:reference, which skips the catalog lookup and resolves directly.
A document can carry its own records or point at a private registry, which also silences unknown-id warnings for that kind:
{
"name": "Branded Deck",
"design": { "colorScheme": "acme-brand" },
"catalogs": {
"colorSchemes": {
"records": [{ "id": "acme-brand", "name": "Acme Brand", "accent1": "#0F4C81", "light1": "#FFFFFF", "dark1": "#0B1B2B" }]
},
"narratives": { "source": "https://catalogs.example.com/narratives" }
},
"slides": [{ "title": "Branded Deck" }]
}Design in one paragraph
design selects a theme (which bundles default color scheme, font scheme, background, and dimensions) and may override any of those directly; Slide.design overrides the deck design per slide. More specific always wins, field by field. Color schemes and font schemes each support two mixable models — OOXML slots/pairs that round-trip to PowerPoint, and abstract roles (primary, heading, code, …) that engines map onto slots. Content color fields (rich-text runs, styled table cells) reference the design system by name — a scheme slot (accent2), a role (text), or a var:<id> entry from the top-level variables map — so styled content follows a re-theme instead of freezing hex values. The full precedence chain with worked examples is in design-resolution.md.
Assets
Binary content lives in the top-level assets registry, keyed by id. Content payloads and design fields reference entries with asset:<id> strings; asset src values accept HTTPS URLs, data URIs, and paths resolved against the OPF file location.
A complete small deck
Everything above, together — intent metadata, a catalog-backed narrative with beats, design, an organization and speaker, an asset-backed chart, regions, notes, and sections:
{
"$schema": "https://openpresentation.org/schema/opf/v1",
"name": "Q3 Business Review",
"description": "Quarterly review for the executive team.",
"audience": "executive",
"purpose": "decide",
"tone": "formal",
"language": "en-US",
"narrative": "qbr",
"takeaway": "Approve the expanded rollout budget.",
"duration": 20,
"organization": {
"id": "acme",
"name": "Acme Corp",
"domain": "acme.com",
"socials": { "linkedin": "acme" }
},
"speaker": { "id": "alice", "name": "Alice Chen", "title": "VP Operations", "organizationId": "acme" },
"design": {
"theme": "classic",
"colorScheme": "forest-green",
"footer": { "left": { "organization": true }, "right": { "slideNumber": true } }
},
"assets": {
"adoption-csv": { "src": "./data/adoption.csv", "alt": "Monthly adoption data" }
},
"slides": [
{
"layout": "title",
"beat": "objectives",
"title": "Q3 Business Review",
"subtitle": "Operations — October 2025"
},
{
"beat": "performance-headline",
"title": "Adoption Doubled",
"left": { "metric": { "value": "2.1x", "label": "Quarter-over-quarter adoption", "trend": "up" } },
"center+right": {
"chart": { "type": "line", "data": { "columns": ["Month", "Active Teams"], "rows": [["Jul", 8], ["Aug", 12], ["Sep", 17]], "source": { "src": "asset:adoption-csv" } } }
},
"notes": "Pause here; this is the slide the decision hangs on."
},
{
"beat": "risks",
"section": "Decision",
"title": "What Could Go Wrong",
"items": [
"Capacity: two regions are at 85% utilization.",
{
"text": "Churn risk in the legacy tier.",
"description": "Mitigation: migration incentives ship in November."
}
]
},
{
"beat": "asks",
"title": "The Ask",
"text": "Approve $1.2M to expand the rollout to all regions in Q4."
}
]
}The beat ids (objectives, performance-headline, risks, asks) come from the qbr narrative record; the theme, color scheme, chart type, and layout all resolve through the bundled catalogs. For a fixture that exercises the full surface in one file, see examples/technical/full-feature-tour.opf.json.
Validation philosophy
Two layers, with a deliberate split:
- Schema errors for structural problems: wrong types, overlapping region keys, payloads mixing incompatible content kinds, a region payload missing concrete content, duplicate slide or payload ids.
- Warnings for advisory drift: unknown catalog ids, unknown
var:variable references and unrecognized run colors, narrative/slide mismatches. These never make a document invalid.
validate from @openpresentation/opf applies both layers locally and, beyond them, checks references, accessibility, layout and content (validate).
Where to go next
schema-reference.md— every field of every object in the presentation schema.catalog-schema-reference.md— every field of every catalog record schema.content-payloads.md— payload shapes and inference rules with examples.design-resolution.md— the design precedence algorithm.examples.md— guide to the example decks underexamples/.
Then write a deck, commit it, revise it, and read the diff. A two-line diff for a two-word change is the whole argument for the format.