---
audience: developers
summary: The JSON document that is your document.
title: Spec format
path: concepts/specs
status: published
---


# Spec format

ScaiScribe's source of truth is a JSON document — the **spec**. Every other artefact (the rendered `.docx`, the PDF, the per-page screenshots) is *derived* from it. Edit the spec, re-render, you get a new artefact. Ingest a document, you get a spec back. The spec is what the system *is*; the binaries are what it *produces*.

The full schema lives at [`schema/scaiscribe.schema.json`](https://github.com/scailabs/scaiscribe/blob/main/schema/scaiscribe.schema.json). This page describes the parts you'll touch when authoring or editing.

## Top-level shape

```json
{
  "schema_version": "0.1",
  "doc_id": "doc_01HXYZ...",
  "type": "docx",
  "theme": "office",
  "metadata": {
    "title": "Q4 Review",
    "author": "Marketing"
  },
  "body": [
    { "type": "heading", "level": 1, "text": "Quarterly review" },
    { "type": "paragraph", "markdown": "Revenue **up 12%** YoY." }
  ],
  "fidelity": { "score": 1.0, "warnings": [] }
}
```

Four fields drive everything:

- **`type`** ∈ `docx | pptx | xlsx`. Drives which renderer runs and which elements are legal in `body`/`slides`/`sheets`.
- **`theme`** — name of a registered theme. Controls typography, colours, page geometry. See [Themes](/docs/scaiscribe/concepts/themes).
- **`body`** (DOCX) / **`slides`** (PPTX) / **`sheets`** (XLSX) — the actual content tree.
- **`fidelity`** — set by the ingester when the spec came from a binary. `score` ∈ [0, 1]; `warnings` is a list of structured records describing structural loss.

## Element model — DOCX `body`

```json
{ "type": "heading", "level": 2, "text": "Section" }
{ "type": "paragraph", "markdown": "Inline **markdown** is supported." }
{ "type": "list", "ordered": false, "items": [{ "text": "alpha" }, { "text": "beta" }] }
{ "type": "table", "headers": ["Q", "Rev"], "rows": [["Q1", "12"], ["Q2", "15"]] }
{ "type": "image", "asset_id": "ast_01HXY...", "alt_text": "Logo" }
{ "type": "callout", "style": "info", "markdown": "Heads up." }
{ "type": "code_block", "language": "python", "content": "def hello():\n    return 'hi'" }
{ "type": "chart", "kind": "column", "title": "Revenue", "data": { "categories": ["Q1","Q2"], "series": [{"name": "EMEA", "values": [10, 12]}] } }
{ "type": "page_break" }
{ "type": "toc", "title": "Contents", "depth": 3 }
```

Every element carries an `elem_id` once it lives in the database (the API mints it). When you submit elements via PATCH `add_element`, you omit the id — the server assigns one and returns the materialised spec.

## Element model — PPTX `slides`

```json
{
  "layout": "title",
  "elements": [
    { "type": "slide_title", "text": "Q4 Review" },
    { "type": "slide_subtitle", "text": "FY26 close" }
  ],
  "notes": "Cover slide; 30 seconds."
}
```

Slide-element types: `slide_title`, `slide_subtitle`, `bullet_list`, `image`, `chart`, `table`. The `layout` matches a theme master id (see [Themes](/docs/scaiscribe/concepts/themes)) — `title`, `content`, `two_column`, `image_feature`, `data_feature`, `section_divider`, `closing` in the default theme.

## Element model — XLSX `sheets`

```json
{
  "name": "Data",
  "freeze": { "cols": 1, "rows": 1 },
  "cells": [
    { "address": "A1", "value": "Region", "style_id": "header_row" },
    { "address": "B1", "value": "Revenue", "style_id": "header_row" },
    { "address": "A2", "value": "EMEA" },
    { "address": "B2", "value": 1450, "number_format": "currency_eur" }
  ],
  "charts": [
    { "anchor": "D2", "kind": "column", "title": "By region", "data": { ... } }
  ],
  "named_ranges": [
    { "name": "RevenueData", "range": "A1:B5" }
  ]
}
```

Cell `style_id` references a theme-defined style block (`header_row`, `alternating_row`, `totals_row`). `number_format` references the theme's format catalogue (`currency_eur`, `percent`, `thousands`).

## Editing — PATCH operations

Don't rewrite the whole spec to make a change. PATCH applies a list of operations atomically:

```json
{
  "operations": [
    { "op": "add_element", "element": { "type": "paragraph", "markdown": "New text" } },
    { "op": "edit_element", "elem_id": "elem_01HX...", "element": { "type": "heading", "level": 2, "text": "Updated title" } },
    { "op": "delete_element", "elem_id": "elem_01HY..." },
    { "op": "move_element", "elem_id": "elem_01HZ...", "after": "elem_01HA..." }
  ]
}
```

For slides and sheets, the matching ops are `add_slide` / `add_slide_element` / `add_sheet` / `add_sheet_chart`. Every op returns the materialised spec with the change applied, so the client always sees what the server now considers canonical.

## Versioning

Each PATCH creates a new spec version internally; the document's `current_version` advances. Ingest replaces the version (the spec becomes whatever the ingester reconstructed). Finalize renders a specific version — usually `current_version`. Old versions stay queryable for 30 days as a recovery aid.

The `schema_version` field is a stability contract. v0.1.x specs are forward-compatible within the v0.1 line; a breaking schema change would bump to `0.2` and ship with a migration policy.

## Why JSON?

- Diffable. You can `git diff` two versions of a spec.
- Validatable. JSON Schema catches structural mistakes at the boundary.
- Language-agnostic. Python, TypeScript, .NET SDKs all generate types from the same schema.
- Editable by AI. The MCP tool surface is structured around making targeted edits to this exact tree.
- Cheap to store. Specs are small (10–500 KB typical); binaries are ~10× larger and derivable.

## Where to next

- [Themes](/docs/scaiscribe/concepts/themes) — how typography and colours apply across formats.
- [Charts](/docs/scaiscribe/concepts/charts) — the dual-emission strategy.
- [Ingestion](/docs/scaiscribe/concepts/ingestion) — what survives when you read a binary back in.
- [Reference → API](/docs/scaiscribe/reference/api) — the REST and MCP endpoints that produce / mutate specs.
