Structured Content
Author rich legal, policy, or compliance documents in YAML or JSON and embed them anywhere in a REFRACT PDF template. At render time, the REFRACT PDF engine fetches the StructuredDocument asset and injects its fully-typed blocks directly into the PDF output. Also hosts the supplementary_Annex section type for per-line-item supporting documentation.
Overview
A Structured Content node embeds a separately-authored document (stored as a YAML or JSON asset) directly inside a REFRACT PDF template. Instead of building long-form text sections (terms and conditions, service agreements, policy clauses) inside the Designer, you write them once as a StructuredDocument asset file and reference it from one or many templates.
When a document is generated, the REFRACT PDF engine pre-fetches the asset before beginning layout composition. The document is then converted to layout blocks and injected in place of the node. All theme colours, TOC integration, and section numbering are applied automatically.
In addition to standard document sections, a structured_content node can host a supplementary_Annex section - a specialised block that appends per-line-item supporting documentation (notes and photo grids) to the end of any REFRACT PDF output.
Node Configuration
Add a structured_content node to your layoutSchema:
{
"id": "node_terms",
"type": "structured_content",
"props": {
"assetPath": "Documents/policies/terms-and-conditions.yaml",
"library": "assets",
"conditionContext": {
"isUK": true,
"tier": "enterprise"
}
}
}| Property | Type | Required | Description |
|---|---|---|---|
assetPath | string | Yes | Path to the YAML or JSON asset within the configured storage library. |
library | string | No | Storage library to read from. Default: "assets". |
conditionContext | object | No | Key/value pairs forwarded to each block's condition evaluator. |
visible | boolean | No | When false, the node is completely skipped and no content is rendered. |
StructuredDocument Format
The asset file (YAML or JSON) must conform to the StructuredDocument shape.
Required Fields
| Field | Type | Description |
|---|---|---|
documentType | string | Document category, printed under the title. Write it as display text, e.g. "Terms and Conditions" or "Master Services Agreement". |
version | string | Semantic version string, e.g. "2.1.0". Printed under the title as v2.1.0. Quote it so YAML keeps it a string. |
title | string | Document title rendered as the level-1 heading (and TOC root entry when TOC is enabled). |
sections | ContentBlock[] | Ordered array of content blocks that make up the document body. |
documentType and version are printed
The renderer prints the document type and version on a line under the title, for example Type: Master Services Agreement · v3.0.0. Write documentType as display text such as "Master Services Agreement", not as an identifier such as "master-services-agreement".
Optional Fields
| Field | Type | Default | Description |
|---|---|---|---|
region | string | Metadata tag, e.g. "UK" or "US". Not rendered; useful for asset management. | |
effectiveDate | string | ISO date string. Not rendered directly but available to conditionContext expressions as needed. | |
tocMode | "flat" | "nested" | "flat" | Controls how headings appear in the document TOC. "flat" includes only level-1 headings; "nested" includes levels 1 and 2 with indentation. |
Minimal YAML Example
documentType: Terms and Conditions
version: "1.0.0"
title: Terms and Conditions
sections:
- type: heading
text: Acceptance of Terms
level: 1
- type: paragraph
text: >
By accessing this service you agree to be bound by these terms.
Please read them carefully before proceeding.
- type: heading
text: Changes to Terms
level: 1
- type: paragraph
text: >
We reserve the right to modify these terms at any time.
Continued use of the service constitutes acceptance.Content Blocks
Every item in sections is a typed block. The type field selects the block renderer. The supplementary_Annex block is documented on its own page. It builds its content from a payload array, and it is not a container for the blocks on this page: only sub_header, repeat_notes, and image_grid can appear in its template. Keep it in its own asset.
image, image_grid and pdf_appendix are also valid top-level block types in sections, for a standalone image or an appended PDF that isn't tied to a payload-driven annex row - see PDF Assets for their full property reference and failure-handling behavior.
heading
Renders a styled section heading. Three depth levels are supported.
| Property | Type | Default | Description |
|---|---|---|---|
text | string | Heading text. Supports inline formatting markers. | |
level | 1 | 2 | 3 | 1 | Depth. Level 1 = 18 pt primary color; level 2 = 15 pt secondary; level 3 = 13 pt border color. |
includeInToc | boolean | Override TOC inclusion. true = always include; false = always exclude; omit = auto (level 1 in flat mode; levels 1-2 in nested mode). | |
keepWithNext | boolean | true | Keeps the heading on the same page as the block after it (a clause, paragraph, list, or note) when that block is at most 2000 characters. Set false to let a heading sit at the foot of a page. |
- type: heading
text: "Liability Limitations"
level: 1
- type: heading
text: "Indirect Damages"
level: 2
includeInToc: falseparagraph
Renders a block of body text.
| Property | Type | Description |
|---|---|---|
text | string | Body text. Supports inline formatting. Use YAML > (folded) or ` |
- type: paragraph
text: >
This agreement is governed by the laws of England and Wales.
Any disputes shall be resolved in the courts of London.
# Literal multi-line (preserves newlines as visible line breaks)
- type: paragraph
text: |
Line one.
Line two indented.
Further indented line.list
Renders a bulleted or numbered list. Items have no hanging indent: a wrapped line starts under the bullet, not under the text. Use clause blocks for content that wraps.
| Property | Type | Description |
|---|---|---|
style | "bullet" | "ordered" | "bullet" = unordered; "ordered" = numbered. |
items | string[] | List item strings. Each item supports inline formatting. |
keepTogether | boolean | By default each item stays on one page. true keeps the whole list on one page. false lets items split. See Keeping blocks together. |
- type: list
style: bullet
items:
- "Full\u00A0**24/7**\u00A0support access"
- Dedicated account manager
- "_Priority_\u00A0response SLA"
- type: list
style: ordered
items:
- Submit a written notice to the registered address.
- Allow 30 days for acknowledgement.
- Escalate to arbitration if unresolved.note
Renders a styled callout box with an icon, accent stripe, and tinted background. Suitable for warnings, disclaimers, or informational asides.
| Property | Type | Default | Description |
|---|---|---|---|
text | string | Body text. Supports inline formatting. | |
variant | "info" | "warning" | "error" | "success" | "important" | "neutral" | "neutral" | Sets the accent colour, background tint, and icon. |
label | string | Capitalised variant name (Note for neutral) | Bold prefix rendered before the note body. The renderer adds a colon, so label: "Reminder" renders as Reminder:. Keep the label to a short noun phrase and put values in text. |
showVariantLabel | boolean | false | Prints the variant name in capitals before the title, for example WARNING: Title. |
title | string | Heading text used with showVariantLabel. Supports inline formatting. | |
keepTogether | boolean | true | A note stays on one page by default. false lets it split. |
Variant Reference
| Variant | Colour | Icon |
|---|---|---|
info | Blue | info |
warning | Amber | warning |
error | Red | x |
success | Green | check |
important | Red | circlealert |
neutral | Secondary | info |
- type: note
variant: warning
text: >
Failure to comply with the data retention schedule may result in
regulatory penalties under applicable data protection law.
- type: note
variant: info
label: "Reminder"
text: "Renew your subscription\u00A0**30 days**\u00A0before expiry to avoid service interruption."clause
Renders a numbered legal clause with an auto-assigned clause number (section.clause). Clause numbers increment automatically within each level-1 heading section. Only level-1 headings advance the section number, so clauses under a level-2 or level-3 heading repeat the section number (two clauses can both read 1.1). Give anything with numbered clauses a level-1 heading, and use a clause title instead of a level-2 heading.
| Property | Type | Description |
|---|---|---|
text | string | Clause body text. Supports inline formatting and {{var}} substitution. |
title | string | Optional bold title prefixed before the clause body, e.g. "Confidentiality". The renderer follows the title with an em dash, then the body. |
subclauses | SubClause[] | Up to one nesting level. Each sub-clause gets a section.clause.subclause number. |
keepTogether | boolean | By default the clause row and each subclause row stay on one page. true keeps the clause and all its subclauses together. false lets it split. See Keeping blocks together. |
SubClause shape
| Property | Type | Description |
|---|---|---|
text | string | Sub-clause text. Supports inline formatting. |
condition | string | Condition key or expression. Sub-clause is skipped when falsy. |
- type: heading
text: Confidentiality
level: 1
- type: clause
title: Definition
text: >
"Confidential Information" means any information disclosed by one party
to the other that is designated as confidential or that reasonably should
be understood to be confidential.
- type: clause
title: Obligations
text: >
Each party agrees to hold the other's Confidential Information
in strict confidence and not to disclose it to any third party.
subclauses:
- text: Not to copy or reproduce Confidential Information without prior consent.
- text: To use the same degree of care as it uses for its own confidential information.
- text: To limit access to personnel with a need to know.
condition: isEnterprisetable
Renders a table of static text: a parties block, a fee schedule, a list of deliverables. Use it for text you write in the YAML. For a table bound to a payload array, use the Table control in the template layout instead.
Both shapes are tagged for Section 508 and PDF/UA, so a screen reader announces each cell together with its header.
- Key-value table. No
header, exactly two columns: a label and a value. The label cell is a row header, so a screen reader reads "Provider, Example Services LLC". - Header table. A
headerrow with any number of columns. The header cells are column headers, and the header row repeats at the top of each page when the table continues onto another page.
| Property | Type | Default | Description |
|---|---|---|---|
rows | string[][] | Required | List of rows, each a list of cell strings. Every row must have as many cells as the table has columns. Write an empty cell as "". |
header | string[] | Column headers. Omit it for a two-column key-value table. | |
widths | (number | "*" | "auto")[] | [130, "*"] for key-value, all "*" for a header table | One entry per column: a number of points, "*" (share the remaining width), or "auto". |
align | ("left" | "right" | "center")[] | All "left" | One entry per column. Use "right" for amounts. |
totalRow | boolean | false | Makes the last row bold on a shaded background. |
keepTogether | boolean | Page-break control. See Keeping blocks together. |
Cells accept inline formatting, {{variables}}, and pipe filters. Like every block, a table accepts a condition.
Rules
- No header means exactly two columns. A table with three or more columns and no
headerfails the render, because a screen reader could not tell the columns apart. - Every row needs as many cells as the table has columns. A short or long row fails the render and names the row.
widthsandalignneed one entry per column. A wrong count or an unknown value fails the render.- Header cells and key-value labels do not wrap. If a fixed-width column is too narrow for its header or label, the renderer logs
[table] ... cannot wrap. Widen the column or shorten the text. - There is no cell merging, no per-cell colour, and no images in cells. Use
supplementary_Annexfor images. - A table has no title field. Put a
headingorparagraphabove it when it needs one.
Example: parties block (key-value)
- type: table
rows:
- ["Provider", "Example Services LLC"]
- ["Client", "Acme Corp"]
- ["SOW reference", "SOW-2026-001"]
- ["Effective date", "The date of the last signature"]
- ["Provider signatory", "Jane Smith, Managing Member"]
- ["Client signatory", "John Doe, Vice President"]There is no header and there are two columns, so this is a key-value table. The default widths give the label column 130 points and the value column the rest.
Example: milestone schedule with a total row
- type: table
header: ["Milestone", "Deliverables", "Due"]
widths: ["*", 100, 100]
align: [left, right, left]
totalRow: true
rows:
- ["Kickoff", "2", "At kickoff"]
- ["Readout and delivery of deliverables", "3", "At readout"]
- ["Total deliverables", "5", ""]The first column takes the remaining width, the deliverables and due columns are 100 points each, and the counts are right-aligned. The last row is bold and shaded. The empty "Due" cell is written "".
Example: values from the payload
- type: table
header: ["Item", "Quantity", "Due date"]
align: [left, right, left]
rows:
- ["{{line1.description}}", "{{line1.qty}}", "{{line1.due|date}}"]
- ["Kickoff workshop", "1", "{{kickoff_date|date}}"]Variables are resolved before the markup is parsed, and pipe filters such as |date work as they do in a paragraph. With a payload of line1.due: "2026-10-01" and no dateFormat in the template settings, the cell renders as 10/1/2026. An unresolved variable becomes an empty string, so a missing value leaves the cell blank without an error.
Example: a longer table that may split across pages
- type: table
header: ["Deliverable", "Owner", "Due"]
widths: ["*", 110, 90]
keepTogether: false
rows:
- ["Current-state assessment", "Provider", "Week 1"]
- ["Stakeholder interviews", "Both parties", "Weeks 1 to 2"]
- ["Architecture options paper", "Provider", "Week 3"]
- ["Proposal for the build", "Provider", "Week 4"]keepTogether: false lets the table break across pages. The header row repeats on the new page, and a row is never cut in half.
Example: a conditional table
- type: table
condition: include_expenses
header: ["Expense", "Rule"]
rows:
- ["Travel", "Agreed in advance, billed at cost"]
- ["Software licenses", "Purchased separately"]The table renders only when include_expenses is truthy in the context. Write the key without braces. See Conditional Blocks.
Common mistakes
Three columns without a header fails the render:
# Fails: a table without "header" must have exactly 2 columns (label, value) but has 3.
- type: table
rows:
- ["Kickoff", "2", "At kickoff"]Add header: ["Milestone", "Deliverables", "Due"].
A row with the wrong number of cells fails the render:
# Fails: row 1 has 2 cells but the table has 3 columns.
- type: table
header: ["Milestone", "Deliverables", "Due"]
rows:
- ["Kickoff", "2"]Write ["Kickoff", "2", ""] for an empty last cell.
Formatting inside a cell
A cell of plain text is fine. As everywhere in a structured document, a plain space between two differently formatted runs inside one cell is not drawn. See the warning under Inline Formatting.
signature_block
Renders one or more signature slots in a table layout. Each signatory gets a REFRACT eSign anchor token, [[SIGN_<ROLE>|required]], that binds the signing zone to a signer by role when the document is sent through REFRACT eSign. Use one signature_block per document: signer numbering restarts in each block, and a document with two blocks has not been tested with a real eSign request.
| Property | Type | Default | Description |
|---|---|---|---|
title | string | Optional heading displayed above the signature table. | |
columns | 1 | 2 | 3 | Auto (from signatory count, max 3) | Number of signature columns per row. When the role text is too wide for the column count, the renderer uses fewer columns so that every signatory still renders. |
signatories | Signatory[] | Single blank slot | Explicit list of signatories. When omitted, a single auto-generated slot is used. |
includeName | boolean | true | Show a printed-name field in the auto-generated slot. |
includeDate | boolean | true | Show a date field in the auto-generated slot. |
Signatory shape
| Property | Type | Description |
|---|---|---|
name | string | Pre-filled printed name. Omit for a blank signature line. |
role | string | Role label displayed above the signature line (e.g. "Authorized Signatory"). Also used to generate the e-sign anchor token [[SIGN_AUTHORIZED_SIGNATORY|required]]. |
date | string | Pre-filled date string. Omit for a blank date field. |
includeInitials | boolean | Adds an initials zone above the signature. The eSign field manifest gets a matching initials field, so the signer captures initials separately from the full signature. |
required | boolean | Whether the zone must be filled even when no requested signer's role maps to it. Absent means required. Applies to both the signature and the initials anchors. |
# Simple auto-generated single slot
- type: signature_block
title: Authorization
includeName: true
includeDate: true
# Multi-signatory with two columns
- type: signature_block
title: Agreed by both parties
columns: 2
signatories:
- role: Customer Representative
- role: Refract Logic Account Manager
name: Jane Smith
date: "2026-03-28"Signers, roles, and order
When the document is sent through REFRACT eSign, each signer in the eSign request is matched to a signatory by role. The match ignores case and treats spaces and underscores alike, so "Example Services LLC" matches example_services_llc. The order in which signers sign is set in the eSign request (the order of the signer list and the signing mode), not by the order of the signatories in this block.
checkbox
Renders a checkbox icon followed by a label. Useful for acknowledgement lists and form-like documents.
| Property | Type | Default | Description |
|---|---|---|---|
label | string | Text displayed beside the checkbox. Supports inline formatting. | |
checked | boolean | false | Renders the checkbox in the checked (filled) state when true. |
- type: checkbox
label: "I have read and understood the\u00A0**Privacy Policy**."
checked: false
- type: checkbox
label: "I consent to receiving email communications."
checked: trueradio
Renders a radio-button icon followed by a label. Use multiple radio blocks with the same group value to represent an option set.
| Property | Type | Default | Description |
|---|---|---|---|
label | string | Text displayed beside the radio button. Supports inline formatting. | |
selected | boolean | false | Renders the radio in the selected (filled) state when true. |
group | string | Semantic group identifier. Metadata only and not rendered. |
- type: radio
label: Standard plan
group: plan
selected: false
- type: radio
label: "**Enterprise**\u00A0plan (includes SLA)"
group: plan
selected: trueinput
Renders a labelled form field. The block is used by portal form steps, and it also prints in the PDF: when the context has a value for fieldKey, the PDF shows that value, and otherwise it shows the field's placeholder.
| Property | Type | Default | Description |
|---|---|---|---|
label | string | Required | Label shown with the field. |
fieldKey | string | Required | Key used to look up the field's value in the render context. |
inputType | "text" | "textarea" | "number" | "date" | "email" | "phone" | "currency" | "select" | Required | The type of input. Drives the portal widget. |
placeholder | string | Hint shown in the portal, and in the PDF when the field has no value. | |
defaultValue | string | Value pre-populated when the form is first opened. | |
readonly | boolean | false | Makes the field read-only in the portal (computed or pre-filled values). |
computed | string | Expression such as {{a.b}} + {{c.d}}, evaluated in the portal. The PDF reads the resolved value from the context. | |
options | (string | { value, label })[] | Choices for inputType: select. A bare string is used as both value and label. | |
validation | object | Portal-side constraints: required, min, max, minLength, maxLength, pattern, and a message shown when validation fails. |
- type: input
label: Contact email
fieldKey: contactEmail
inputType: email
placeholder: name@example.com
validation:
required: true
message: Enter a valid email address.attachment
Renders a file-attachment field. The block is used by portal form steps. In the PDF it shows an [Attachment] placeholder until a file is submitted, then lists the attached file.
| Property | Type | Default | Description |
|---|---|---|---|
label | string | Required | Label for the attachment zone. |
fieldKey | string | Required | Key used to look up the attachment details in the render context. |
accept | string | Any file | Comma-separated MIME types or file extensions the portal accepts, e.g. "application/pdf,.pdf,.docx". |
maxSizeMb | number | Maximum file size in megabytes, enforced by the portal. | |
required | boolean | false | Whether at least one file must be attached before the step can be submitted. |
- type: attachment
label: Signed purchase order
fieldKey: purchaseOrder
accept: ".pdf"
maxSizeMb: 10
required: truepage_break
Inserts a hard page break. Subsequent blocks start at the top of a new page. A step_boundary block renders as a page break too. Do not end a document with a page_break: it adds a blank last page that holds only the header and footer.
- type: page_breakInline Formatting
All text-bearing blocks (paragraph, heading, list items, clause, note, checkbox, radio) support inline formatting markers.
| Syntax | Effect |
|---|---|
**bold** | Bold text |
_italic_ | Italic text |
__underline__ | Underlined text |
==highlight== | Highlighted text (theme-tinted background) |
{{section:Heading Text}} | Cross-reference link to a heading with matching text |
Markers do not nest. Unmatched open markers are treated as plain text. Markers also match inside words: SOW_REF_2026_001 renders as the italic SOWREF2026001, and a pair of == in prose highlights the text between them. There is no escape syntax, so keep raw underscores and paired == out of identifiers and prose.
Spaces next to formatted text
In the current renderer, a plain space between two differently formatted runs is not drawn. Full **24/7** support renders as "Full24/7support". Put a non-breaking space (U+00A0) where each space would be. In a double-quoted YAML string, write it as , for example text: "Full **24/7** support access". Spaces inside a run, and text that is entirely one format, are not affected. This applies to paragraphs, clauses, list items, notes, checkbox and radio labels, and table cells.
Variable Substitution
Use {{varName}} or {{dot.path}} placeholders in any text field. At render time these are replaced with matching values from conditionContext (or the merged context from the payload). An unresolved variable renders as an empty string, and no warning is produced. Supply every variable the document uses, and list them in a comment at the top of the YAML.
- type: paragraph
text: "This agreement is entered into by\u00A0**{{customerName}}**\u00A0(the \"Customer\") and Refract Logic LLC on {{effectiveDate}}."
- type: clause
text: >
The agreed monthly fee is {{pricing.monthlyFee}} per seat,
billed on the {{billing.dayOfMonth}}th of each month.Substitution is resolved from the merged context in priority order:
- Raw payload fields (lowest priority)
conditionContextset innode.propsconditionContextfrompayloadData.conditionContext(global override)payloadData.structuredContentOverrides[nodeId].conditionContext(per-node, highest priority)
Field Names with Dots or Spaces
The substitution engine uses greedy longest-match, so field names containing dots (.) or spaces are supported without any escaping:
- type: paragraph
text: "Order {{Header.No.}} raised on {{Header.Order Date}}."{{Header.No.}} resolves the literal key "No." on Header, and {{Header.Order Date}} resolves the key "Order Date" - neither requires quoting or encoding in the template.
Pipe Filters
Append a pipe filter to a placeholder to format its value at render time:
- type: paragraph
text: "Effective {{effectiveDate|date}} - total {{summary.amount|currency}} for {{summary.count|number}} items."| Filter | Effect |
|---|---|
|date | Formats an ISO 8601 date string using templateSettings.dateFormat ("M/D/YYYY" when dateFormat is not set). |
|currency | Formats a number as a locale-aware currency string using templateSettings.locale and templateSettings.currency. |
|number | Formats a number with locale-appropriate thousands separators and decimal notation. |
ISO 8601 Auto-detection
Date fields that contain a valid ISO 8601 string (e.g. "2026-03-29" or "2026-03-29T00:00:00Z") are auto-detected by the |date filter - no inputDateFormat configuration is required.
Conditional Blocks
Any block can carry a condition field. The block is skipped entirely when the condition evaluates to falsy.
- type: note
variant: warning
condition: isUK
text: "This section applies exclusively to customers domiciled in the United Kingdom."
- type: clause
condition: "pricing.monthlyFee > 5000"
title: Enterprise Discount
text: "Customers with a monthly fee exceeding £5,000 qualify for the enterprise discount programme."Condition Forms
| Form | Example | Behaviour |
|---|---|---|
| Simple key | isUK | Truthy when context.isUK is truthy |
| Dot path | customer.isPremium | Resolves nested object paths |
| Comparison | "summary.total > 10000" | Numeric comparison (>, <, >=, <=, ===, !==) |
| String match | "tier == enterprise" | Loose equality against a string literal |
YAML Quoting
YAML values containing > or < must be quoted: condition: "summary.total > 5000".
Unquoted > in YAML is a block scalar indicator and will produce unexpected results.
What conditions cannot do
A condition is a single key or a single comparison. There is no negation (!isUK) and no && or ||. A condition the evaluator cannot resolve is falsy, so the block is hidden without an error. Write a plain key without braces: condition: isUK works, but condition: "{{isUK}}" is falsy. To express "not", use isUK != true, which is true when the key is missing or false. isUK == false is true only when the key is exactly false, so it hides the block when the key is missing.
Payload-Level Overrides
Without modifying the .rpkg template, you can control individual structured_content nodes from the render payload.
Global context override
Applies to all structured_content nodes in the document:
{
"templateId": "service-agreement-v2",
"conditionContext": {
"isUK": true,
"tier": "enterprise"
},
"customerName": "Acme Corp"
}Per-node override (by node id)
Targets a specific node and takes highest priority:
{
"templateId": "service-agreement-v2",
"structuredContentOverrides": {
"node_terms": {
"visible": false
},
"node_dpa": {
"conditionContext": {
"isGDPR": true
}
}
}
}| Override Key | Type | Description |
|---|---|---|
visible | boolean | false hides the node entirely. true clears a template-level visible: false. |
conditionContext | object | Merged at the highest priority into the node's condition evaluator context. |
TOC Integration
When templateSettings.toc.enabled is true, heading blocks from structured_content nodes are automatically merged into the document-level Table of Contents.
- Flat mode (
tocMode: "flat"or default): only level-1 headings appear as TOC entries. - Nested mode (
tocMode: "nested"): level-1 headings are parents; level-2 headings are indented children.
includeInToc: false on an individual heading excludes it regardless of mode.
When a structured_content node hosts a supplementary_Annex section with includeInToc: true, each qualifying line item emits a TOC sub-entry that is merged into the same document TOC at the correct depth. These entries appear as children of the annex title heading and inherit the active sectionNumbering prefix (e.g. 2.1, 2.2).
Section Numbering
When templateSettings.sectionNumbering is true, level-1 headings inside structured_content blocks receive auto-incrementing ordinal prefixes (1., 2., 3.) that are continuous with the rest of the document's section counter.
Typed heading numbers
Clause numbers come from the heading count (1.1, 1.2, 2.1), whatever sectionNumbering is set to. If your headings already contain typed numbers, for example "1. Purpose", leave templateSettings.sectionNumbering off. With it on, the renderer prefixes level-1 headings again and the output reads 1.1. Purpose.
Pagination
Pagination hints from the surrounding node are respected:
pagination setting | Effect |
|---|---|
breakBefore: "always" | Forces a page break before the structured content block. |
breakBefore: "whenOverflow" | Behaves the same as always for layout purposes. |
breakAfter: "always" | Forces a page break after the block. |
keepTogether: true | Marks the entire block as unbreakable and tries to keep it on one page. |
Keeping blocks together
Blocks also control where they may break across pages:
- Clauses, subclauses, and list items stay on one page by default, so a number never sits on one page with its text on the next. A row longer than 2000 characters may still split, so split a very long clause into several.
- A heading stays on the same page as the block after it (a clause, paragraph, list, or note) when that block is at most 2000 characters. Set
keepWithNext: falseon the heading to let it sit at the foot of a page. A heading before a longer block, a signature block, an input, or anything else can still be left at the foot of a page: put apage_breakbefore it. - A table or a note stays on one page by default. A table of more than 2000 characters may split, and rows never split.
keepTogether: trueon a clause keeps the clause and all its subclauses on one page, and on a list keeps the whole list on one page. It must fit on a page: a block taller than a page is laid out splittable and a warning is logged, instead of failing the render.keepTogether: falselets a clause, list, table, or note split across pages.
A kept-together block that does not fit in the space left on a page moves whole to the next page and leaves a gap. That is intended.
Accessibility
Structured content is tagged for Section 508 and PDF/UA. The structure a screen reader sees follows the block types:
| Block | Tagged as |
|---|---|
heading | A heading one level deeper than its visual level: block level 1, 2, and 3 become H2, H3, and H4, below the document title. |
clause | A list item (L, LI, Lbl, LBody) with the clause number as the label. Subclauses are further items in the same list. A clause is a list item even when it is the only clause in its section. |
note | One paragraph, wherever it sits. It is never a list item, even directly after a clause. |
table | A table with header cells, as described under table. |
list | A list. A list with a single item is a paragraph. |
checkbox, radio | Consecutive ones form a list. A lone checkbox or radio is a paragraph. |
AI-assisted accessibility checks
Some checkers classify content by its appearance and may report that a clause alone in its section "looks like a heading". The list tagging is intentional, and these AI-assisted warnings are advisory.
Known Limitations
These are behaviours of the current renderer, not authoring mistakes. Author around them until the renderer changes.
| Limitation | Author around it by |
|---|---|
A condition has no negation (!key) and no && or ||. | Use key != true, which is true when the key is missing or false. See Conditional Blocks. |
| A plain space between differently formatted runs is not drawn. | Use a non-breaking space ( ). See Inline Formatting. |
_italic_ and ==highlight== also match inside words, so SOW_REF_2026_001 loses its underscores. | Keep raw underscores and paired == out of identifiers and prose. There is no escape syntax. |
Clauses under a level-2 or level-3 heading repeat the section number (two clauses can both read 1.1). | Use level-1 headings for anything that has numbered clauses, and a clause title instead of a level-2 heading. |
| Lists have no hanging indent: a wrapped line starts under the bullet, not under the text. | Use clause blocks for content that wraps. |
| An unresolved variable renders as an empty string, with no warning. | Supply every variable, and list them in a comment at the top of the YAML. |
Signer numbering restarts in each signature_block. A document with two blocks has not been tested with a real eSign request. | Use one signature_block per document. |
| There are no glyphs for Chinese, Japanese, Korean, or Arabic text: it renders as empty boxes. Latin-extended, Greek, and Cyrillic render correctly. | Confirm font coverage before using non-Latin party names. |
| Type sizes cannot be set from the template: body text is 10 pt, headings are 18, 15, and 13 pt, and notes are 9 pt. | Nothing today. A size change is an engine change. |
A page_break at the end of a document adds a blank last page holding only the header and footer. | Do not end a document with a page_break. |
| A heading is kept with the block after it only for a clause, paragraph, list, or note of at most 2000 characters. | Put a page_break before a heading that precedes a longer block, a signature block, or an input. |
Designer Preview
In the Designer (which has no asset access), the node renders a visible grey placeholder:
[Structured content: Documents/policies/terms-and-conditions.yaml]If pre-fetching fails inside the Designer, a red error message is shown instead:
[Structured content error: Documents/policies/terms-and-conditions.yaml]
HTTP 404: The specified resource does not exist.This red inline error is Designer-only. In a real server render, the asset-resolution step runs before layout and re-throws any fetch failure as an error that fails the whole render, rather than rendering the document with an inline placeholder for that node - check the render error for the asset path and HTTP status rather than expecting a partial PDF.
The grey and red placeholders above apply to the whole structured_content node, regardless of what block types it contains - the Designer's browser-side preview (playground-client.js) never runs the asset-resolution step at all, since it has no server-side storage credentials, so it can't distinguish block types within a node it hasn't fetched. This is unrelated to image/image_grid/pdf_appendix block rendering, which is a real server-render concern, not a preview one - see PDF Assets for that rendering behavior.
Full Document Example
A complete master-services-agreement.yaml illustrating all block types:
documentType: Master Services Agreement
version: "3.0.0"
title: Master Services Agreement
region: UK
effectiveDate: "2026-01-01"
tocMode: nested
sections:
# Section 1
- type: heading
text: Definitions
level: 1
- type: paragraph
text: >
In this Agreement the following terms shall have the meanings set out below.
All other capitalised terms are defined where they first appear.
- type: clause
title: Agreement
text: >
Means this Master Services Agreement, including all Schedules and
Order Forms incorporated by reference.
- type: clause
title: Confidential Information
text: >
Means any information disclosed in connection with this Agreement that
is designated confidential or that a reasonable person would consider
confidential given the circumstances of disclosure.
# Section 2
- type: heading
text: Services
level: 1
- type: heading
text: Scope of Services
level: 2
- type: paragraph
text: >
Refract Logic LLC shall provide the services set out in each Order Form
executed under this Agreement (**"Services"**).
- type: note
variant: info
text: >
Service specifications, SLAs, and pricing are defined in the relevant
Order Form and Schedule A attached hereto.
# Section 3 (conditional)
- type: heading
text: Data Protection (UK/EU)
level: 1
condition: isGDPR
- type: paragraph
condition: isGDPR
text: >
The parties agree to comply with the UK GDPR and any applicable
successor legislation in respect of all personal data processed
under this Agreement.
- type: note
variant: warning
condition: isGDPR
text: >
A Data Processing Addendum (**DPA**) must be signed separately before
any personal data is transferred between the parties.
# Section 4
- type: heading
text: Fees and Payment
level: 1
- type: clause
title: Fees
text: "The Customer shall pay the fees set out in the applicable Order Form. The monthly fee of\u00A0**{{pricing.monthlyFee}}**\u00A0per seat is invoiced in advance on the first business day of each calendar month."
subclauses:
- text: All fees are exclusive of VAT and other applicable taxes.
- text: "Enterprise customers (>£5,000/month) receive a 5% volume discount."
condition: "pricing.monthlyFee > 5000"
# Acknowledgement
- type: heading
text: Acceptance
level: 1
- type: paragraph
text: >
By executing this Agreement the parties confirm they have read,
understood, and agree to be bound by all terms herein.
- type: checkbox
label: "Customer has received and reviewed Schedule A (Service Specifications)."
checked: false
- type: checkbox
label: "Customer has received and reviewed Schedule B (Data Processing Addendum)."
condition: isGDPR
checked: false
- type: page_break
- type: signature_block
title: Signatures
columns: 2
signatories:
- role: Customer Representative
- role: Refract Logic Authorized SignatoryRelated Pages
- Supplementary Annex - per-line-item supporting documentation section hosted inside a
structured_contentnode - Template Settings -
toc,sectionNumbering,dateFormat, and locale options that affect structured content rendering - Hierarchical Table - the main data table whose Notes/Images columns are suppressed when a supplementary annex is active
- Table - the
Tablecontrol, for tables bound to a payload array. Thetableblock on this page is for static text written in the YAML
Icon Reference
Complete reference of valid icon names available for use in REFRACT PDF documents. Includes status, information, content, and system icons.
Supplementary Annex
Generate a per-line-item supporting documentation section at the end of any REFRACT PDF output. The supplementary_Annex block flattens hierarchical order-line data, fetches and downsamples images, and renders a themed header, notes, and photo grid for every qualifying item - all gated behind a single feature flag.