Data & variants
Docs/Data & variants

Handlebars syntax reference

Bring params and props into your template with Handlebars, from displaying text to building tables and conditional content.

9 min read15 sections
On this page

When designing a template, you often need to bring data into its content: a recipient’s name from params, a heading from props, or a list of products displayed as table rows. Tembuilder uses Handlebars to connect these values to your HTML/CSS. Its syntax lets you mark where data should appear and describe how content should be displayed.

For example, write {{ params.recipient }} where the recipient’s name belongs. When you preview the document or generate a PDF, Tembuilder reads the matching value and replaces the expression with the actual content. This lets you reuse the same design with different sets of data.

This guide takes you through reading values from Parameters and Props, showing content conditionally, repeating lists, and using advanced syntax when your template needs more complex data structures.

1. From template to document

Start with a simple example: you have created a section prop heading with the value Invoice and declared a parameter recipient with the value Tembuilder. In the section’s HTML, place each value where you want it to appear using {{ ... }}:

<h1>{{ props.heading }}</h1>
<p>{{ params.recipient }}</p>
<h1>Invoice</h1>
<p>Tembuilder</p>
{{ props.heading }} → Invoice{{ params.recipient }} → Tembuilder
→
TembuilderInvoice

Tembuilder

Expressions are replaced before PDF pagination.

2. Choose the correct data path

Path Reads
{{ params.recipient }} Data supplied for this run, after defaults are applied.
{{ props.heading }} Props for the part being compiled. In a section, section values override Global values with the same name.
{{ global.props.company }} Global props explicitly, even when a section overrides the same name.
{{ @root.params.recipient }} Run data from inside a loop or with block.

Use explicit params. paths instead of bare parameter names. Root parameter names are also exposed for compatibility, but names such as props, params, and global are occupied by platform context.

Each section, Header, and Footer is compiled with its own context. @root means the root of that compilation, not another section. Variants select prop overrides before expressions run.

3. Text, numbers, booleans, and missing values

The following object is run data: enter its fields in Run without adding an outer params wrapper. Declare matching types in Parameters → Schema.

{
  "recipient": "Tembuilder & Tembuilder",
  "quantity": 2,
  "balance": 0,
  "paid": false,
  "totalLabel": "250,000 VND"
}
<p>{{ params.recipient }} · {{ params.quantity }}</p>
<p>{{ params.totalLabel }}</p>
{{#if params.paid}}Paid{{else}}Awaiting payment{{/if}}

Text displays as text, numbers display their value, and directly printing a Boolean produces true or false. Use a condition for a reader-friendly label. Keep 2 numeric and false Boolean; "2" and "false" are strings.

An unresolved value normally prints nothing. That is not a substitute for declaring required fields or validating run data. Invalid syntax or an unknown helper stops compilation.

Handlebars does not calculate quantity * price, call .toFixed(2), or format dates/currencies automatically. Prepare totals and formatted strings before rendering. {{ params.quantity + 1 }} and {{ params.total.toFixed(2) }} are not supported JavaScript expressions.

4. Objects and property names

{
  "recipient": { "name": "Tembuilder", "address": { "city": "Hanoi" } },
  "labels": { "invoice_title": "Invoice", "postal_code": "10000" }
}
<p>{{ params.recipient.name }} · {{ params.recipient.address.city }}</p>
<h2>{{ params.labels.invoice_title }}</h2>
<p>{{ params.labels.postal_code }}</p>

Use field names such as recipientName or postal_code. The editor replaces spaces, hyphens, and other characters outside letters, digits, and underscores with _; the first character must be a letter or underscore. For an existing imported schema with keys such as postal code, Handlebars can read them using {{ params.labels.[postal code] }}. That syntax does not change the editor’s naming rules. To render an object meaningfully, select its fields or iterate them; printing the whole object is not JSON serialization.

5. Conditions: if, else, unless

{{#if params.paid}}
  <p>Payment received</p>
{{else if params.overdue}}
  <p>Payment overdue</p>
{{else}}
  <p>Awaiting payment</p>
{{/if}}
{{#unless params.paid}}<p>Please pay before the due date.</p>{{/unless}}
{{#if params.balance includeZero=true}}<p>Balance: {{ params.balance }}</p>{{/if}}

if takes the negative branch for false, null, missing values, an empty string, zero, and an empty array. includeZero=true makes zero take the positive branch. An empty object and the string "false" take the positive branch. unless reverses the condition. if does not change the current context.

There are no built-in eq, gt, and, or or helpers in this platform. Compute a Boolean such as overdue in the input instead of writing {{#if params.balance > 0}}.

6. Arrays: each and empty states

{
  "items": [{ "name": "Design", "quantity": 2 }, { "name": "Review", "quantity": 1 }],
  "tags": ["Draft", "Internal"]
}
<table>
  <thead><tr><th>Item</th><th>Quantity</th></tr></thead>
  <tbody>
    {{#each params.items}}
      <tr><td>{{ this.name }}</td><td>{{ this.quantity }}</td></tr>
    {{else}}
      <tr><td colspan="2">No items</td></tr>
    {{/each}}
  </tbody>
</table>
{{#each params.tags}}<span>{{ this }}</span>{{/each}}

this is the current item: an object for items, a string for tags. Direct array access is possible with {{ params.items.[0].name }}. For long tables, follow pagination guidance.

{{#each params.items}} {{ this.name }} · {{ this.quantity }}{{else}} No items {{/each}}
ItemQuantity
Design2
Review1
One item → one row. An empty list → the else branch.

7. Loop metadata and object iteration

Expression Meaning inside each
@index Array position, starting at zero.
@first, @last First/last array item flags.
@key Property name when iterating an object.
{{#each params.tags}}
  {{#if @first}}<strong>Tags:</strong>{{/if}}
  {{ this }}{{#unless @last}}, {{/unless}}
{{/each}}
{{#each params.recipient}}
  <p>{{ @key }}: {{ this }}</p>
{{/each}}

Do not use @index + 1 to create row numbers. Supply a rowNumber field if you need numbering that starts at one.

8. Context: with, parent paths, and root

{{#with params.recipient}}
  <p>{{ name }}</p>
{{else}}
  <p>No recipient</p>
{{/with}}
{{#each params.items}}
  <p>{{ this.name }} — {{ ../props.heading }}</p>
  <small>{{ @root.global.props.company }}</small>
{{/each}}

with and each change context. ../ returns to the parent context; @root provides stable access to the compilation root across nested loops. Adding an if does not require another ../.

Aliases make nested lists easier to follow. Here, declare groups as an array of objects, each with a name and an items array:

{{#each params.groups as |group groupIndex|}}
  <h2>{{ group.name }}</h2>
  {{#each group.items as |item itemIndex|}}
    <p>{{ groupIndex }}.{{ itemIndex }} — {{ item.name }}</p>
  {{/each}}
{{/each}}

9. Dynamic lookup and subexpressions

Use lookup for a key supplied as data. Declare labels as an object and selectedKey as Text; for { "labels": { "approved": "Approved" }, "selectedKey": "approved" }:

<p>{{ lookup params.labels params.selectedKey }}</p>
{{#with (lookup params.labels params.selectedKey)}}<strong>{{ this }}</strong>{{/with}}

Parentheses nest a helper result inside another helper. They do not evaluate arbitrary JavaScript. Quoted arguments are literal strings, while unquoted paths read data. includeZero=true is a named helper option, not an assignment to input data.

10. HTML escaping and CSS values

<p>{{ params.recipient }}</p>

For recipient equal to Tembuilder & Tembuilder, the HTML source contains Tembuilder &amp; Tembuilder, and the document displays Tembuilder & Tembuilder. Keep double braces for ordinary input.

{{{ params.trustedHtml }}} and {{& params.trustedHtml }} insert HTML without escaping. Only use them for trusted, sanitized markup you intend to become document elements. Escaping is not URL or CSS validation. Validate values intended for href, src, or styles.

In the CSS editor, a controlled Color prop can be read directly:

h1 { color: {{ props.accent }}; }

Give accent a valid color such as #2563eb. Handlebars provides substitution; the result still needs to be valid CSS.

11. Comments, whitespace, and literal braces

{{! Internal note, omitted from output }}
{{!-- A longer note that can include {{ expressions }} --}}
<span>{{ params.recipient ~}}</span>

~ next to a delimiter trims adjacent whitespace on that side. Use it carefully around words that need a separating space. Handlebars removes its own comments during compilation. Tembuilder also removes HTML comments during sanitization, so neither type remains in the final document HTML.

To show a literal expression rather than evaluate it:

<p>\{{ params.recipient }}</p>

The document displays {{ params.recipient }}. Raw blocks ({{{{raw}}}}…{{{{/raw}}}}) require a registered block helper; this platform does not provide a raw helper.

12. Partials and other advanced features

An inline partial can reuse markup inside one compiled source:

{{#*inline "recipientCard"}}
  <p>{{ name }}</p>
{{/inline}}
{{> recipientCard params.recipient}}

It is not shared automatically across section files, Headers, or Footers. {{> sharedCard}} needs a definition; Tembuilder does not expose a registry for user-defined global partials or custom JavaScript helpers.

Handlebars also supports partial parameters, dynamic partial selection, partial blocks, and custom block helpers. These are language capabilities, not a promise of platform registration features. Do not paste registerHelper, registerPartial, or script tags into template HTML expecting them to configure compilation.

The built-in log helper writes to the execution environment’s log rather than the PDF. Prefer Preview and Run to inspect output; avoid logging sensitive run data. Decorators and prototype/method access are not needed for document authoring: use plain JSON values and the documented helpers.

13. Complete section example

Create section prop heading = "Invoice" and Global prop company = "Tembuilder". Declare recipient (Object with name: Text), items (Array of Object with name: Text and quantity: Number), paid (Boolean), and totalLabel (Text). Run with:

{
  "recipient": { "name": "Tembuilder" },
  "items": [{ "name": "Design", "quantity": 2 }],
  "paid": false,
  "totalLabel": "250,000 VND"
}

Paste into the section HTML:

<h1>{{ props.heading }}</h1>
<p>{{ global.props.company }} → {{ params.recipient.name }}</p>
<ul>
  {{#each params.items}}
    <li>{{ this.name }} × {{ this.quantity }}</li>
  {{else}}
    <li>No items</li>
  {{/each}}
</ul>
<p>Total: {{ params.totalLabel }}</p>
<p>{{#if params.paid}}Paid{{else}}Awaiting payment{{/if}}</p>

Expected content: Invoice, Tembuilder → Tembuilder, Design × 2, Total: 250,000 VND, Awaiting payment. Try an empty items array and paid: true to verify both alternate branches in Preview/Run.

14. Troubleshooting and AI authoring rules

Symptom Check
Empty value Field spelling, declared type, run payload, active variant, and current context.
Wrong value inside a loop Use this, an alias, ../, or @root to select the intended context.
Zero hidden Use includeZero=true where zero is a valid displayed value.
Unexpected positive condition Check for a string "false" instead of Boolean false.
Compile error Match every block’s opening/closing names; remove JavaScript operators and unavailable helpers/partials.
[object Object] Select object fields or iterate them instead of printing the whole object.

For AI authors: preserve declared field names and types, use explicit namespaces, compute business logic before rendering, provide empty states, keep ordinary values escaped, and verify examples against both populated and empty data. A missing value does not prove a successful binding. See common errors.

15. Official Handlebars references

Use the official Handlebars guide for the full language documentation. The platform behavior described above takes precedence when an upstream example requires host-side registration or runtime configuration.

Search documentation