6. Data binding and expressions
A binding is a placeholder in double braces, {{ … }}, that is replaced with data when the PDF is rendered. This chapter covers
everything you can write inside one: field paths, preview text, calculations, conditions and formatting.
Note: All examples in this chapter use the sample shipment
examples-data.json. The text after→is exactly what the example prints, and every one is verified automatically against the engine.
Where you can use expressions
| Where | What you type | Example |
|---|---|---|
| Text elements, table cells, headers and footers, group headers, barcode values, image sources, links, PDF title | Template text: normal text with {{ … }} placeholders |
Ship to: {{ shipTo.name }} |
| Visible if, Checked if, a table's or repeater's Data, Sort by, Group by, One copy per | A bare expression, without braces | hazmat |
Your first bindings
Your system sends data as JSON. Here is part of the sample shipment:
{
"orderNumber": "SO-104412",
"shipTo": { "name": "Northwind Retail #1187", "city": "Columbus", "state": "OH", "zip": "43215" },
"items": [ { "sku": "APP-TS-1001", "qty": 24, "weight": 0.4 }, "…" ]
}
To print a value, write its path. A path is the field names joined with dots:
{{ orderNumber }} → SO-104412
{{ shipTo.name }} → Northwind Retail #1187
{{ shipTo.city }}, {{ shipTo.state }} {{ shipTo.zip }} → Columbus, OH 43215
Spaces inside the braces are optional: {{shipTo.city}} and {{ shipTo.city }} mean the same thing.
Tip: Don't type paths you can drag. The Fields tab creates the correct binding for you (chapter 3).
Suggestions while you type
When you type a name inside {{ … }} (or anywhere in a bare expression box such as Visible if), a list of suggestions opens under the box:
- Fields from your application's data contract. After a dot, the list shows that object's fields: type
shipTo.to seename,city, … - Functions, with their arguments and what they do, for example
formatCurrency (n, [code]). Picking one insertsformatCurrency()with the cursor between the parentheses. - Built-in names that apply in that box:
page,pagesandnoweverywhere;itemandindexin a table's cells and in a repeater;rowsin a table's footer;groupin group headers and footers. In a table's cells,item.lists the fields of the table's rows.
Use Up and Down to choose, Enter or Tab to insert, and Esc to close the list. Ctrl+Space opens it even before you type.
Preview text: {{ field | preview text }}
The designer never has real data. To make the design readable, write sample text after a |:
{{ customer.name | ACME Corp }} → Northwind Retail
- In the designer and its preview, this shows ACME Corp.
- In a real render, the whole placeholder is replaced by the data (here Northwind Retail). The preview text is ignored.
- Without preview text, the designer shows the binding itself,
{{customer.name}}. - If the data has no value, a real render prints nothing in its place, never the preview text, so sample text can't leak into a real
document. To print a default, use
coalesce.
Tip: Make preview text realistic in length. The longest name your customers have ("Northwind Retail Distribution Center #1187") shows you whether the box is wide enough. Preview PDF then shows realistic line breaks.
Everything after the first | is preview text, with its surrounding spaces trimmed. Two exceptions: a | inside quotes, and || (the "or" operator), don't count.
Paths in detail
{{ items[1].sku }} → ELE-PB-2000
{{ shipTo['dock door'] }} → Door 3
{{ $.orderNumber }} → SO-104412
{{ poNumbers[0] }} → PO-780100
| Syntax | Meaning |
|---|---|
a.b.c |
Field c inside b inside a |
list[0] |
First item of a list (counting starts at 0) |
a['field name'] |
A field whose name has spaces, dashes or other special characters, e.g. shipTo['zip-code'] |
$ |
The top of the data (useful inside tables, where item is the current row) |
A path that doesn't exist is simply empty. It never causes an error.
Special variables
| Variable | Value | Where |
|---|---|---|
page |
The current page number | Anywhere |
pages |
The total number of pages | Anywhere |
now |
The date and time of the render | Anywhere |
item |
The current row's data | Table cells, repeater elements, Sort by, Group by |
index |
The current row number, counting from 0 | Table cells, repeater elements |
rows |
All rows of the table (in footers) or of the group (in group headers and footers) | Table footers, group headers and footers |
group |
The current group: group.key, group.index (counting from 0) and group.count (rows in the group) |
Group headers and footers |
record, recordIndex |
In a batch, the current record and its number (from 0) | Anywhere, with One copy per set |
Page {{ page }} of {{ pages }} → Page 2 of 3
Line {{ index + 1 }}: {{ item.sku }} → Line 1: APP-TS-1001
{{ group.key }} ({{ group.count }} lines) → Apparel (2 lines)
Operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ - * / % |
Arithmetic (% = remainder) |
cartons * 2 |
4 |
+ with text |
Joins text | 'Door ' + 3 |
Door 3 |
== != |
Equal / not equal ('10' == 10 is true) |
status == 'Shipped' |
true |
=== !== |
Strictly equal / not equal (same type) | cartons === 2 |
true |
< <= > >= |
Comparisons | totalWeight > 1000 |
true |
&& || ! |
And, or, not | hazmat && cartons > 1 |
true |
?? |
Use the right side when the left is missing (null/undefined) | notes ?? 'None' |
None |
a ? b : c |
If a then b, otherwise c |
hazmat ? 'HAZMAT' : '' |
HAZMAT |
( ) |
Grouping | (1 + 2) * 3 |
9 |
cartons * 2 → 4
'Door ' + 3 → Door 3
status == 'Shipped' → true
cartons === 2 → true
totalWeight > 1000 → true
hazmat && cartons > 1 → true
notes ?? 'None' → None
hazmat ? 'HAZMAT' : '' → HAZMAT
(1 + 2) * 3 → 9
Text values go in single or double quotes: 'Prepaid' or "Prepaid".
Warning: Calculations with decimals can show tiny rounding errors, as on any computer:
24 * 4.85gives116.39999999999999. Always format money and weights withformatNumberorformatCurrency:
{{ items[0].qty * items[0].unitCost }} → 116.39999999999999
{{ formatCurrency(items[0].qty * items[0].unitCost, 'USD') }} → $116.40
Functions
Functions are written name(argument, argument, …). The function reference lists them all; every expression box has a
Functions link under it that opens the reference in Help. A function name the designer doesn't know is flagged under the box as you type, with a
suggestion when it's close to a real one: Unknown function 'formatCurency' (did you mean formatCurrency?). The most useful functions are below.
Totals and counts over a list
sum, avg, min and max take a list and the name of the field to add up. count counts items.
{{ count(items) }} → 3
{{ sum(items, 'qty') }} → 82
{{ max(items, 'qty') }} → 48
{{ min(items, 'qty') }} → 10
{{ formatNumber(sum(items, 'weight'), 2) }} lb → 1.63 lb
{{ formatCurrency(avg(items, 'unitCost'), 'USD') }} → $9.67
Inside a table footer, use rows instead of the list name, e.g. {{ sum(rows, 'qty') }}. See Tables.
Numbers
{{ formatNumber(totalWeight, 1) }} → 2,942.5
{{ formatNumber(totalWeight, 0) }} → 2,942
{{ round(totalWeight, 2) }} → 2942.46
{{ floor(totalWeight) }} → 2942
{{ ceil(totalWeight) }} → 2943
{{ formatCurrency(declaredValue, 'USD') }} → $1,250.00
formatNumber(value, decimals) adds thousands separators and fixes the number of decimals. formatCurrency(value, code) takes a three-letter
currency code such as USD, EUR, GBP, MXN, CNY, JPY or KRW. Both follow the Document language (see below).
Dates and times
formatDate(value, pattern) accepts dates like "2026-10-18", date-times like "2026-10-20T15:45:00", and timestamps.
{{ formatDate(orderDate, 'MM/DD/YYYY') }} → 10/18/2026
{{ formatDate(orderDate, 'DD.MM.YY') }} → 18.10.26
{{ formatDate(orderDate, 'MMMM D, YYYY') }} → October 18, 2026
{{ formatDate(orderDate, 'ddd MMM D') }} → Sun Oct 18
{{ formatDate(shipDate, 'MM/DD/YYYY h:mm A') }} → 10/20/2026 3:45 PM
{{ formatDate(shipDate, 'YYYY-MM-DD HH:mm') }} → 2026-10-20 15:45
{{ formatDate(shipDate, '[Shipped] MMM D') }} → Shipped Oct 20
Printed {{ formatDate(now, 'MM/DD/YYYY') }} → Printed 10/20/2026
| Token | Prints | Token | Prints |
|---|---|---|---|
YYYY |
2026 | HH / H |
15 / 15 (24-hour) |
YY |
26 | hh / h |
03 / 3 (12-hour) |
MMMM |
October | mm |
45 (minutes) |
MMM |
Oct | ss |
00 (seconds) |
MM / M |
10 / 10 | A |
AM / PM |
DD / D |
08 / 8 | dddd / ddd |
Sunday / Sun |
[text] |
the text as-is |
Warning: Time values that include a time zone (
…Zor…+02:00) are converted to the time zone of the computer that renders the PDF. If your system sends local warehouse times, it should send them without a zone ("2026-10-20T15:45:00"). Your integrator can confirm this.
Text
{{ upper(shipTo.city) }} → COLUMBUS
{{ lower('PREPAID') }} → prepaid
{{ pad(carrier.proNumber, 10, '0') }} → 0078121007
{{ truncate(items[0].description, 20) }} → Organic cotton crew…
{{ substr(sscc, 1, 7) }} → 0614141
{{ replace(orderNumber, 'SO-', '') }} → 104412
{{ join(poNumbers, ' / ') }} → PO-780100 / PO-780137 / PO-780174
{{ concat(shipTo.city, ', ', shipTo.state) }} → Columbus, OH
{{ len(poNumbers) }} → 3
Empty and missing values
coalesce returns the first value that is not empty. Use it for defaults:
{{ coalesce(notes, 'No special instructions') }} → No special instructions
{{ coalesce(shipTo.attention, 'Receiving') }} → Receiving
{{ item.hazmat ?? 'n/a' }} → n/a
?? only replaces missing values (null), while coalesce also skips empty text. A field sent as "" is skipped by coalesce but kept by ??.
Showing and hiding elements: Visible if
Behavior → Visible if takes a bare expression. The element prints only when the expression is "truthy":
| Value | Visible? |
|---|---|
true, any non-zero number, any non-empty text, a non-empty list |
Yes |
false, 0, empty text, missing, an empty list |
No |
hazmat
notes
count(items) > 20
freightTerms == 'collect' && cod.amount > 0
page == pages
The last one prints an element only on the last page. A hidden element still takes up its space. Elements below it don't move up.
Tip: To show one of two texts, use one element with a condition inside it instead of two overlapping elements:
{{ hazmat ? 'Contains dangerous goods' : 'No dangerous goods' }}.
Formatting for other languages
formatNumber, formatCurrency and formatDate follow the Document language set on the Report tab (see International documents).
The same template prints like this in other languages (French and German use non-breaking spaces as separators, so numbers never break across lines):
# locale: fr-FR
{{ formatNumber(totalWeight, 1) }} kg → 2 942,5 kg
{{ formatDate(orderDate, 'dddd D MMMM YYYY') }} → dimanche 18 octobre 2026
# locale: de-DE
{{ formatCurrency(declaredValue, 'EUR') }} → 1.250,00 €
# locale: ko-KR
{{ formatDate(orderDate, 'YYYY년 M월 D일 dddd') }} → 2026년 10월 18일 일요일
# locale: zh-CN
{{ formatDate(orderDate, 'YYYY年M月D日') }} → 2026年10月18日
{{ formatCurrency(declaredValue, 'CNY') }} → ¥1,250.00
# locale: ja-JP
{{ formatCurrency(declaredValue, 'JPY') }} → ¥1,250
Recipes
Address block that skips an empty line. Put each line in the same text element, separated by Enter, and skip the attention line when it is empty:
{{ shipTo.name }}{{ shipTo.attention ? ' / Attn: ' + shipTo.attention : '' }} → Northwind Retail #1187
{{ shipTo.address1 }} → 2250 Lakeshore Blvd
Singular and plural.
{{ cartons }} {{ cartons == 1 ? 'carton' : 'cartons' }} → 2 cartons
Zero-padded document numbers (for example for a barcode that must be 10 digits):
{{ pad(carrier.proNumber, 10, '0') }} → 0078121007
Totals with units.
Total: {{ formatNumber(sum(items, 'qty'), 0) }} units, {{ formatNumber(totalWeight, 1) }} lb → Total: 82 units, 2,942.5 lb
A "Page X of Y" footer with print date.
Page {{ page }} of {{ pages }} · Printed {{ formatDate(now, 'MM/DD/YYYY h:mm A') }} → Page 2 of 3 · Printed 10/20/2026 2:30 PM
A tracking link. In Behavior → Link: https://track.example.com/?pro={{ carrier.proNumber }}.
Conditional label text on a BOL.
{{ freightTerms == 'prepaid' ? 'Freight charges are PREPAID' : 'Freight charges are COLLECT' }} → Freight charges are PREPAID
When something goes wrong
- The designer shows an error under the box. The expression can't be read, often because of a missing quote or parenthesis, or it calls a function that doesn't exist. The message says where, and suggests the right function name when it can.
- The value prints empty. The path doesn't match the data. Check the spelling and capitalization (
shipTois notShipTo). Look for the field in the Fields tab. - The preview warns "Expression '…': …". The expression ran into a problem while it was calculated and printed empty.
These are wrong:
formatNumber(weight, 2
{{ 'Door + 3 }}
{{ items[0.sku }}
More fixes are in Troubleshooting.