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.
{
"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
| Field | Meaning |
|---|---|
id | Stable unique id. Regenerated on duplicate/paste. |
type | Block type from the registry: built-in (heading, table…) or custom (acme.risk-profile). |
props | Block-specific properties. Binding props hold expressions as strings. |
style | Optional per-instance style overrides (fontSize, color, padding, border, textStyle…). |
children | For containers: sections hold rows/blocks, rows hold columns, columns hold blocks/rows. |
visibleWhen | A condition string or structured rule group. |
flow | breakBefore, breakAfter, keepTogether, keepWithNext, minOrphans, minWidows. |
name, locked | Layer 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:
{ "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.