Skip to content

Document schema

The saved template is human-readable JSON and a public API. It is validated with a zod schema (parseTemplate) and versioned with schemaVersion.

This is a real document, not a sketch: it parses. JSON has no comments, so where a section list would be long it is simply shown empty rather than filled with a /* … */ that would make the file invalid if you pasted it.

json
{
  "schemaVersion": 1,
  "id": "tpl_x1",
  "name": "Client Investment Review",
  "page": {
    "size": "A4",
    "orientation": "portrait",
    "margins": { "top": 16, "right": 18, "bottom": 16, "left": 18 },
    "header": { "height": 18, "differentFirstPage": false },
    "footer": { "height": 16, "differentFirstPage": false }
  },
  "styles": {
    "text": { "h1": { "fontFamily": "$typography.headingFont", "fontSize": 26, "color": "$colors.primary", "keepWithNext": true }, "body": { "fontSize": 11 } },
    "table": { "borders": "horizontal", "headerBackground": "$table.headerBackground" },
    "card": { "borderRadius": 6, "padding": 16 },
    "link": { "color": "$colors.secondary" },
    "spacing": { "blockGap": 8, "sectionPadding": 0, "columnGap": 16 }
  },
  "header": { "default": [] },
  "footer": { "default": [] },
  "body": [
    {
      "id": "sec_1", "type": "section", "props": { "padding": 0 },
      "children": [
        { "id": "n_1", "type": "heading", "props": { "text": "Hello {{ client.firstName }}", "level": "h1" } },
        {
          "id": "row_1", "type": "row", "props": { "layout": [6, 6] },
          "children": [
            { "id": "col_1", "type": "column", "props": { "span": 6 }, "children": [ { "id": "n_2", "type": "kpi", "props": { "label": "Value", "value": "portfolio.value | currency" } } ] },
            { "id": "col_2", "type": "column", "props": { "span": 6 }, "children": [] }
          ]
        }
      ]
    }
  ],
  "settings": { "locale": "en-GB", "currency": "GBP" }
}

Node

FieldMeaning
idStable unique id. Regenerated on duplicate/paste.
typeBlock type from the registry: built-in (heading, table…) or custom (acme.risk-profile).
propsBlock-specific properties. Binding props hold expressions as strings.
styleOptional per-instance style overrides (fontSize, color, padding, border, textStyle…).
childrenFor containers: sections hold rows/blocks, rows hold columns, columns hold blocks/rows.
visibleWhenA condition string or structured rule group.
flowbreakBefore, breakAfter, keepTogether, keepWithNext, minOrphans, minWidows.
name, lockedLayer label; lock against editing.

Units: page geometry in millimetres; everything else in CSS pixels at 96 dpi. Colours and fonts are literals or theme tokens ($colors.primary, $typography.headingFont, $table.headerBackground, $logo).

Rich text

Text blocks store a structured document, never HTML:

json
{ "type": "doc", "content": [ { "type": "paragraph", "content": [
  { "type": "text", "text": "Dear " },
  { "type": "token", "expr": "client.firstName" },
  { "type": "text", "text": ",", "marks": { "bold": true } }
] } ] }

Marks: bold, italic, underline, strike, code, link, color. Lists are bulletList/orderedList of listItem paragraphs.

Row layouts

Twelfths: [12], [6,6], [4,4,4], [3,3,3,3], [8,4], [4,8], [9,3], [3,9]. Any combination summing to 12 is valid. Rows are atomic across pages unless allowSplit is set or the row is taller than a page.

Unknown blocks

A template referencing a block that is not registered still loads, validates with an unknown-block error, renders a placeholder and round-trips its props untouched. Uninstalling a custom block never corrupts saved documents.