Liquid reference
The Liquid template language in CastPDF, the money, number and format_date filters, escaping, the sandbox limits and how errors report line and column.
On this page
Templates use Liquid, rendered with LiquidJS 10. Liquid runs on a template's HTML, and on the html of POST /v1/pdf when the request has a data object. It does not run on CSS.
Basics
{{ … }} outputs a value, {% … %} runs a tag, and | applies a filter. The keys of your data object are the template's variables.
| Template | Data | Output |
|---|---|---|
Hello {{ customer.name }} | {"customer":{"name":"Ada"}} | Hello Ada |
[{{ missing }}] | (none) | [] |
{% for item in items %}{{ forloop.index }}. {{ item.name | upcase }} {% endfor %} | {"items":[{"name":"Design"},{"name":"Build"}]} | 1. DESIGN 2. BUILD |
{% if paid %}Paid{% else %}Due{% endif %} | {"paid":false} | Due |
{% assign total = items | map: "amount" | sum %}{{ total | money: "USD" }} | {"items":[{"amount":40},{"amount":2.5}]} | $42.50 |
{{ note | default: "No notes" }} | (none) | No notes |
- A variable that does not exist outputs nothing, rather than failing.
- Only your data's own properties are visible to the template.
- These tags are available:
if,elsif,else,unless,case,for(withforloop,limit,offset,reversed,break,continue),tablerow,assign,capture,increment,decrement,cycle,echo,liquid,rawandcomment. - Partials are not supported:
include,renderandlayoutfail with a template error. Keep everything in one template.
Standard filters
All of LiquidJS's standard filters work, for example upcase, capitalize, truncate, replace, split, join, size, first, last, sort, where, map, sum, plus, minus, times, divided_by, round, default and date. The LiquidJS filter reference documents them all. A filter name that does not exist is an error (when you save a template, or when the HTML is rendered), never a silent no-op.
CastPDF filters
Three filters format values for documents, using JavaScript's built-in internationalisation (Intl). Each takes an optional locale, a BCP 47 tag such as en-US, de-DE or fr-CH, defaulting to en-US.
money
{{ value | money: currency, locale }} formats an amount in a currency.
| Argument | Default | Description |
|---|---|---|
currency | "EUR" | An ISO 4217 currency code, such as "USD", "EUR", "GBP" or "JPY". |
locale | "en-US" | The locale that decides the symbol's position, separators and spacing. |
The value may be a number or a numeric string. The number of decimals follows the currency (two for USD and EUR, none for JPY, rounded). Note the default currency: write the currency explicitly when it is not the euro.
| Template | Data | Output |
|---|---|---|
{{ total | money }} | {"total":99} | €99.00 |
{{ total | money: "USD" }} | {"total":1234.5} | $1,234.50 |
{{ total | money: "EUR", "de-DE" }} | {"total":1234.5} | 1.234,50 € |
{{ total | money: "GBP", "en-GB" }} | {"total":"19.9"} | £19.90 |
{{ total | money: "JPY", "ja-JP" }} | {"total":1234.5} | ¥1,235 |
Some locales put a no-break space between the amount and the symbol (as in 1.234,50 €), which keeps them on the same line in the PDF.
number
{{ value | number: decimals, locale }} formats a number with grouping separators and a fixed number of decimals.
| Argument | Default | Description |
|---|---|---|
decimals | 2 | Digits after the decimal separator, from 0 to 10. The value is rounded. |
locale | "en-US" | The locale's grouping and decimal separators. |
| Template | Data | Output |
|---|---|---|
{{ 1234.5 | number }} | (none) | 1,234.50 |
{{ 3.14159 | number: 2 }} | (none) | 3.14 |
{{ 1234.5 | number: 1, "de-DE" }} | (none) | 1.234,5 |
{{ qty | number: 0 }} | {"qty":"42"} | 42 |
format_date
{{ value | format_date: style, time_zone, locale }} formats a date.
| Argument | Default | Description |
|---|---|---|
style | "medium" | "full", "long", "medium" or "short". Any other value is an error. |
time_zone | "UTC" | An IANA time zone such as "Europe/Berlin" or "America/New_York". The date is shown as it is in that zone. |
locale | "en-US" | The language and order of the date. |
The value may be an ISO 8601 string (such as "2026-09-28" or "2026-09-28T10:00:00Z") or a number of milliseconds since 1970.
| Template | Data | Output |
|---|---|---|
{{ issued | format_date }} | {"issued":"2026-09-28T10:00:00Z"} | Sep 28, 2026 |
{{ issued | format_date: "long" }} | {"issued":"2026-09-28T10:00:00Z"} | September 28, 2026 |
{{ issued | format_date: "short" }} | {"issued":"2026-09-28T10:00:00Z"} | 9/28/26 |
{{ issued | format_date: "full", "Europe/Berlin", "de-DE" }} | {"issued":"2026-09-28T23:30:00Z"} | Dienstag, 29. September 2026 |
The last example shows why the time zone matters: 23:30 UTC on 28 September is already 29 September in Berlin.
A value that is not a number (for money and number) or not a date (for format_date), a malformed currency code or an unknown time zone fails the render with template_render_error.
Escaping
Output is HTML-escaped by default, so data such as <b> or & in a customer name cannot break or change your HTML. Use the raw filter to output HTML you trust, such as a formatted address you built yourself:
| Template | Data | Output |
|---|---|---|
{{ note }} | {"note":"<b>Paid</b>"} | <b>Paid</b> |
{{ note | raw }} | {"note":"<b>Paid</b>"} | <b>Paid</b> |
Sandbox limits
Templates run in an isolated sandbox with no access to files or the network. Each render has these limits:
| Limit | Value |
|---|---|
| Time | 1 second per render. A slower render stops with the message "Template rendering exceeded 1 second". |
| Memory | 64 MB. A render that needs more stops with "Template rendering exceeded memory limits". |
| Template size | 500 KB of HTML (a stored template, or the html of POST /v1/pdf when it has data). |
| Tags and outputs | At most 10,000 {{ }} outputs and {% %} tags together in one template. More fails with "The template has more than 10,000 tags and outputs". |
| Output size | 5 MB of HTML (with the CSS) after Liquid. Over it, the request fails with document_too_large. |
Exceeding the time, memory or tag limit is a template_render_error. Checking a template when you save it has the same time limit. These limits apply to Liquid only; the PDF rendering that follows has its own limits.
Errors and line numbers
A template that cannot be parsed or rendered fails with 422 and the code template_render_error. When LiquidJS knows where the problem is, details gives the line and column in the template's HTML, counted from 1:
{
"error": {
"code": "template_render_error",
"message": "Template error: undefined filter: moneyy",
"docs_url": "https://castpdf.com/docs/errors#template_render_error",
"details": { "line": 2, "column": 1 }
}
}The position usually points at the start of the {{ … }} or {% … %} that failed. Syntax errors and unknown filters are caught when you save a template, so they do not reach your production renders; errors that depend on the data (such as money on a value that is not a number) appear when a document is rendered.