Template Components

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"
    }
  }
}
PropertyTypeRequiredDescription
assetPathstringYesPath to the YAML or JSON asset within the configured storage library.
librarystringNoStorage library to read from. Default: "assets".
conditionContextobjectNoKey/value pairs forwarded to each block's condition evaluator.
visiblebooleanNoWhen 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

FieldTypeDescription
documentTypestringDocument category, printed under the title. Write it as display text, e.g. "Terms and Conditions" or "Master Services Agreement".
versionstringSemantic version string, e.g. "2.1.0". Printed under the title as v2.1.0. Quote it so YAML keeps it a string.
titlestringDocument title rendered as the level-1 heading (and TOC root entry when TOC is enabled).
sectionsContentBlock[]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

FieldTypeDefaultDescription
regionstringMetadata tag, e.g. "UK" or "US". Not rendered; useful for asset management.
effectiveDatestringISO 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.

PropertyTypeDefaultDescription
textstringHeading text. Supports inline formatting markers.
level1 | 2 | 31Depth. Level 1 = 18 pt primary color; level 2 = 15 pt secondary; level 3 = 13 pt border color.
includeInTocbooleanOverride TOC inclusion. true = always include; false = always exclude; omit = auto (level 1 in flat mode; levels 1-2 in nested mode).
keepWithNextbooleantrueKeeps 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: false

paragraph

Renders a block of body text.

PropertyTypeDescription
textstringBody 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.

PropertyTypeDescription
style"bullet" | "ordered""bullet" = unordered; "ordered" = numbered.
itemsstring[]List item strings. Each item supports inline formatting.
keepTogetherbooleanBy 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.

PropertyTypeDefaultDescription
textstringBody text. Supports inline formatting.
variant"info" | "warning" | "error" | "success" | "important" | "neutral""neutral"Sets the accent colour, background tint, and icon.
labelstringCapitalised 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.
showVariantLabelbooleanfalsePrints the variant name in capitals before the title, for example WARNING: Title.
titlestringHeading text used with showVariantLabel. Supports inline formatting.
keepTogetherbooleantrueA note stays on one page by default. false lets it split.

Variant Reference

VariantColourIcon
infoBlueinfo
warningAmberwarning
errorRedx
successGreencheck
importantRedcirclealert
neutralSecondaryinfo
- 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.

PropertyTypeDescription
textstringClause body text. Supports inline formatting and {{var}} substitution.
titlestringOptional bold title prefixed before the clause body, e.g. "Confidentiality". The renderer follows the title with an em dash, then the body.
subclausesSubClause[]Up to one nesting level. Each sub-clause gets a section.clause.subclause number.
keepTogetherbooleanBy 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

PropertyTypeDescription
textstringSub-clause text. Supports inline formatting.
conditionstringCondition 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: isEnterprise

table

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 header row 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.
PropertyTypeDefaultDescription
rowsstring[][]RequiredList 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 "".
headerstring[]Column headers. Omit it for a two-column key-value table.
widths(number | "*" | "auto")[][130, "*"] for key-value, all "*" for a header tableOne 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.
totalRowbooleanfalseMakes the last row bold on a shaded background.
keepTogetherbooleanPage-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 header fails 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.
  • widths and align need 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_Annex for images.
  • A table has no title field. Put a heading or paragraph above 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.

PropertyTypeDefaultDescription
titlestringOptional heading displayed above the signature table.
columns1 | 2 | 3Auto (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.
signatoriesSignatory[]Single blank slotExplicit list of signatories. When omitted, a single auto-generated slot is used.
includeNamebooleantrueShow a printed-name field in the auto-generated slot.
includeDatebooleantrueShow a date field in the auto-generated slot.

Signatory shape

PropertyTypeDescription
namestringPre-filled printed name. Omit for a blank signature line.
rolestringRole label displayed above the signature line (e.g. "Authorized Signatory"). Also used to generate the e-sign anchor token [[SIGN_AUTHORIZED_SIGNATORY|required]].
datestringPre-filled date string. Omit for a blank date field.
includeInitialsbooleanAdds 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.
requiredbooleanWhether 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.

PropertyTypeDefaultDescription
labelstringText displayed beside the checkbox. Supports inline formatting.
checkedbooleanfalseRenders 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: true

radio

Renders a radio-button icon followed by a label. Use multiple radio blocks with the same group value to represent an option set.

PropertyTypeDefaultDescription
labelstringText displayed beside the radio button. Supports inline formatting.
selectedbooleanfalseRenders the radio in the selected (filled) state when true.
groupstringSemantic 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: true

input

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.

PropertyTypeDefaultDescription
labelstringRequiredLabel shown with the field.
fieldKeystringRequiredKey used to look up the field's value in the render context.
inputType"text" | "textarea" | "number" | "date" | "email" | "phone" | "currency" | "select"RequiredThe type of input. Drives the portal widget.
placeholderstringHint shown in the portal, and in the PDF when the field has no value.
defaultValuestringValue pre-populated when the form is first opened.
readonlybooleanfalseMakes the field read-only in the portal (computed or pre-filled values).
computedstringExpression 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.
validationobjectPortal-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.

PropertyTypeDefaultDescription
labelstringRequiredLabel for the attachment zone.
fieldKeystringRequiredKey used to look up the attachment details in the render context.
acceptstringAny fileComma-separated MIME types or file extensions the portal accepts, e.g. "application/pdf,.pdf,.docx".
maxSizeMbnumberMaximum file size in megabytes, enforced by the portal.
requiredbooleanfalseWhether 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: true

page_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_break

Inline Formatting

All text-bearing blocks (paragraph, heading, list items, clause, note, checkbox, radio) support inline formatting markers.

SyntaxEffect
**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:

  1. Raw payload fields (lowest priority)
  2. conditionContext set in node.props
  3. conditionContext from payloadData.conditionContext (global override)
  4. 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."
FilterEffect
|dateFormats an ISO 8601 date string using templateSettings.dateFormat ("M/D/YYYY" when dateFormat is not set).
|currencyFormats a number as a locale-aware currency string using templateSettings.locale and templateSettings.currency.
|numberFormats 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

FormExampleBehaviour
Simple keyisUKTruthy when context.isUK is truthy
Dot pathcustomer.isPremiumResolves 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 KeyTypeDescription
visiblebooleanfalse hides the node entirely. true clears a template-level visible: false.
conditionContextobjectMerged 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 settingEffect
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: trueMarks 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: false on 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 a page_break before 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: true on 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: false lets 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:

BlockTagged as
headingA heading one level deeper than its visual level: block level 1, 2, and 3 become H2, H3, and H4, below the document title.
clauseA 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.
noteOne paragraph, wherever it sits. It is never a list item, even directly after a clause.
tableA table with header cells, as described under table.
listA list. A list with a single item is a paragraph.
checkbox, radioConsecutive 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.

LimitationAuthor 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 Signatory

  • Supplementary Annex - per-line-item supporting documentation section hosted inside a structured_content node
  • 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 Table control, for tables bound to a payload array. The table block on this page is for static text written in the YAML

On this page