Skip to content

Handlebars templates

An HTML template is a template.html file written with Handlebars 4.7, such as {{invoice.number}}. Its Page header (header.html) and Page footer (footer.html) are Handlebars too, with the same data, helpers and partials. Excel workbook templates also use Handlebars; their cell and row helpers are in spreadsheet templates.

New to templates? Start with build a template.

template.html
<h1>Invoice {{invoice.number}}</h1>
{{#each lines}}
{{component "components/line-row.html" currency=../currency}}
{{/each}}
<p>Total: {{sum lines "amount"}} {{currency}}</p>
  • In Studio’s preview, the data is a sample data file, chosen in the Data box of the preview toolbar.
  • In production, it is the data your application sends with the render request.
  • A missing value prints nothing and the render goes on. Test optional values with {{#if}}.
  • A template whose output is empty fails with 400 BAD_REQUEST (“The template rendered nothing.”) instead of producing a blank PDF.
  • {{> line-row}} looks for a partial file named line-row, line-row.hbs, line-row.html or line-row.handlebars in the template’s folder, then in a partials folder there. See how paths resolve.

Besides the standard Handlebars helpers (if, unless, each, with, lookup, log), templates have:

Helper What it does Details
qrCode, barcode Draw a QR code or barcode from a value. QR codes and barcodes
component Renders a partial file as a component. Below
localize Prints a label from the active translation file. Translations
pdfFormField Adds a field the reader fills in. Form fields
pdfCreatePagesGroup, pdfAddPageItem, pdfDest Page data and link targets for other templates. Combine templates

There are no formatting helpers: format numbers, dates and amounts in your own helpers; see translations and formatting. A helper of your own named qrCode, barcode or one of the pdf… helpers replaces the built-in one. Do not name your own helpers component or localize: the built-in ones win.

{{component "path"}} renders a partial file with the caller’s current data merged with the arguments you pass:

{{#each lines}}
{{component "components/line-row.html" currency=../currency}}
{{/each}}
  • Inside the partial, that merged object is also @root.
  • The partial does not see the caller’s loop variables, such as @index. Pass them as arguments.

To build a whole document from JSON building blocks with no HTML, see component templates.

In Studio, open the template’s helpers.js tab, under the HTML. Every top-level function is a helper:

helpers.js
function sum(items, field) {
return items.reduce((total, item) => total + Number(item[field] ?? 0), 0);
}
<p>Total: {{sum lines "amount"}}</p>
  • Helpers work as the Handlebars guide describes: hash arguments, block helpers, Handlebars.registerHelper, Handlebars.SafeString and functions on module.exports all work.
  • Helpers must return their value directly. An async helper, or one that returns a pending promise, fails with “Helper … must return synchronously”.
  • require("moment") formats dates and require("bwip-js") draws barcodes. See translations and formatting and QR codes and barcodes.
  • To share code with other templates, paste in a workspace file with {#asset ./lib/money.js @encoding=utf8}. The path starts from the file’s own folder.

A script is a workspace file with a beforeRender function. It runs before the template and can change the data, for example to sort lines or compute totals. Use a helper to format one value where it is printed; use a script when several places need the same computed values.

  1. In Studio’s explorer, open the menu of the template’s folder, choose New entity → Script and name it totals.js. Change req.data in beforeRender:

    totals.js
    function beforeRender(req, res) {
    const lines = req.data.lines ?? [];
    req.data.total = lines.reduce((sum, line) => sum + line.quantity * line.price, 0);
    }
  2. Open the template, click Template files (the folder icon in the preview toolbar) and, under Scripts, tick totals.js.

  3. Print the new value in the template, such as <p>Total: {{total}}</p>, and click Update preview.

  4. Save the template and click Publish. Production renders use the script from this version on.

async function beforeRender(req, res) and the callback form beforeRender(req, res, done) work too. A script without beforeRender fails with 400 BAD_REQUEST, and one that declares afterRender fails with 400 UNSUPPORTED_FEATURE.

Field Change Meaning
req.data Yes The render data. Must stay JSON.
req.context Yes Starts empty. Passes values between scripts; it does not reach the template.
req.template.name No The template’s name.
req.template.localization.language No The locale in the template’s page.json.
req.options.localization.language Yes The language of the render. Set it to render in another language.
req.template.pdfMeta Yes PDF properties: title, author, subject, creator, producer, keywords and language.
req.template.pdfPassword Yes Passwords and permissions. Absent when the template has none.
title.js
function beforeRender(req, res) {
req.template.pdfMeta = {
...req.template.pdfMeta,
title: `Invoice ${req.data.invoice.number}`,
};
}
  • req.template.pdfMeta starts with the PDF properties in page.json, keywords joined by commas. What a script sets replaces them, and the request’s own metadata wins over both. Setting pdfMeta.language changes only the PDF’s language, not the translation file.
  • req.template.pdfPassword holds password, ownerPassword, printing (lowResolution or highResolution; absent when printing is not allowed), and modifying, copying, annotating, fillingForms, contentAccessibility and documentAssembly as true or false. A script that sets it replaces the template’s passwords entirely, so permissions it leaves out are not granted. The request’s own encryption still wins. See PDF standards and security.
  • A language a script sets must be a language tag such as de-CH; anything else fails with 400 BAD_REQUEST. How the language picks a translation file is in translations.
  • res and any other change to req are discarded.

The button in a script’s header sets when it runs:

Studio label Runs
Runs where included (the default) Only for templates that tick it under Template files → Scripts.
Runs before renders in this folder Before every render of a template in the script’s folder or below.
Runs before every render Before every render of an HTML or workbook template in the workspace.
  • “Runs before every render” scripts go first, then folder scripts, each group in name order, then the template’s own scripts in the order ticked. They share one req, so a later script sees what earlier ones stored.
  • Two scripts with the same name, or more scripts than the limits allow, fail the render with 400 BAD_REQUEST.
  • Component and spreadsheet (sheets.json) templates run no scripts and have no helpers.js.
  • Editing, adding or rescoping a script changes previews at once, but production renders only after you publish the template again.

Self-hosted installations only. Scripts read per-environment values from process.env. They come from the SCRIPT_ENV line in the server’s .env file, as comma-separated NAME=value pairs:

.env
SCRIPT_ENV=ADD_WATERMARK=true,COMPANY_CODE=ACME
  • process.env holds only these values, nothing else from the server. Every workspace’s scripts on the installation can read them, and scripts cannot change them.
  • Names start with a letter or _ and use letters, digits and _, each once. Values cannot contain commas, and surrounding spaces are trimmed.
  • Values are always text: compare with process.env.ADD_WATERMARK === "true". An unset name reads as undefined.
  • Only scripts see process.env; helpers.js and the template do not. Pass a value on through req.data.
  • A changed value applies once the server restarts with it, in previews too. The template needs no new publish.

For a worked example, see different settings per environment.

Helpers, scripts and templates run in a sandbox:

  • There is no network, file system, timers or browser API, and no npm packages: require gives you moment and bwip-js. Send everything a template needs in the data.
  • Modern JavaScript syntax and built-ins work, except Intl.
  • Variables in helpers.js do not carry over between renders.
  • console.log and the log helper write to the Profiler tab of Studio’s preview.

Time, memory and size limits are in limits.

A mistake in a script, helper or template fails the preview or render. In Studio, the error appears in the preview pane and your edits stay. Through the API, it comes in the usual error envelope. A helper that throws shows its own message after Handlebars render failed:.

Status and code Typical message What to do
400 BAD_REQUEST Invalid Handlebars template: Parse error on line 4… Fix the template syntax at the reported line.
400 BAD_REQUEST helpers.js: Unexpected token (3:14) Fix the JavaScript syntax in helpers.js.
400 BAD_REQUEST Handlebars render failed: Missing helper: "fmt" Define the helper, or fix its name.
400 BAD_REQUEST Script "totals.js": cannot read property… Fix the named script. Check it against data shaped like the request’s.
400 BAD_REQUEST The template rendered nothing. Check the conditions around your content and the data you sent.
400 LIMIT_EXCEEDED Template execution limit exceeded. Look for endless loops, or move heavy work out of the template.
400 LIMIT_EXCEEDED Template memory limit exceeded. Send less data, or avoid building large strings and arrays.
400 UNSUPPORTED_FEATURE Script "x.js": afterRender is not supported.… Remove afterRender; only beforeRender runs.