Appearance
Data Expressions
Press uses {{ }} syntax to insert dynamic values into documents.
Syntax
| Expression | Description | Example |
|---|---|---|
{{ data.field }} | Top-level data field | {{ data.title }} |
{{ data.nested.field }} | Nested object field | {{ data.client.name }} |
{{ data.items[0] }} | Array element by index | {{ data.rows[0].name }} |
{{ item.field }} | Current repeat item | {{ person.email }} |
{{ attrs.name }} | Component attribute | {{ attrs.title }} |
{{ assets.data.field }} | Asset data | {{ assets.data.defaults.currency }} |
{{ section.data.field }} | Flow group data | {{ section.data['chapter-title'] }} |
Aggregating Over Arrays
Five functions reduce an array to a single number, so totals do not have to be computed before the payload is sent.
| Call | Result |
|---|---|
{{ sum(data.items) }} | Total of an array of numbers |
{{ sum(data.items, 'amount') }} | Total of one property across the items |
{{ avg(data.items, 'score') }} | Mean of one property |
{{ min(data.bids, 'price') }} | Smallest value |
{{ max(data.bids, 'price') }} | Largest value |
{{ count(data.items) }} | Number of items |
sum(), avg(), min() and max() take an optional second argument: a quoted property name, dotted to reach a nested field — {{ sum(data.rows, 'pricing.total') }}. It must be a name you type, not an expression. count() takes the array alone.
Plain numeric text counts as a number, so "12.50" adds up the same as 12.50 — but text carrying a thousands separator or a currency symbol ("1,200.50", "£10") does not, and is rejected. Send amounts as raw numbers and format them with <value> on the page.
sum() and count() of an empty array are 0. avg(), min() and max() need at least one item. Anything that cannot be added up is rejected rather than skipped, so a total is never quietly short.
The result is a plain, unrounded number: the mean of 1, 2 and 2 is 1.6666666666666667, and that is what reaches the page unless you round it. Wrap any figure a reader will see — money above all — in <value>:
xml
<p>Subtotal: <value separator="true" dp="2">{{ sum(data.lineItems, 'amount') }}</value></p>
<p>Average score: <value dp="1">{{ avg(data.items, 'score') }}</value></p>Ternary Expressions
{{ condition ? valueIfTrue : valueIfFalse }}| Pattern | Example |
|---|---|
| Default value | {{ data.name ? data.name : 'Unknown' }} |
| Conditional text | {{ data.urgent ? 'URGENT' : 'Standard' }} |
| Conditional sizing | {{ data.sidebar ? '60%' : '100%' }} |
| Conditional style | {{ data.active ? 'bold' : 'normal' }} |
Logical Operators
Combine conditions with &&, || and !. && binds tighter than ||, and both bind looser than a comparison, so data.a == 1 && data.b == 2 groups the way it reads. ! binds tightest of all, tighter than a comparison, so it negates the single term to its right — negate a comparison with {{ !(data.count == 0) }}. Parenthesise to override any of it.
Papermill accepts && written directly. && is the strictly valid XML spelling, which an editor or formatter may expect; both mean the same thing. || and ! need neither.
| Pattern | Example |
|---|---|
| Both | {{ data.active && data.verified }} |
| Either | {{ data.admin || data.owner }} |
| Negation | {{ !data.archived }} |
| Grouped | {{ (data.admin || data.owner) && data.active }} |
What counts as true
The three operators test truthiness. Absent, null, false, 0 and "" are falsy; everything else is truthy — including the strings "0" and "false", and an empty array or object.
An empty list is therefore truthy. Test one for emptiness with {{ count(data.items) == 0 }}, not {{ !data.items }}.
Carrying a value
&& and || stop at the operand that settles the answer and carry its value rather than a true/false. That makes || a fallback:
{{ data.nickname || data.name }}It falls back on any falsy value, not only a missing one, so a 0 or an empty string takes the fallback too. Where zero is a figure worth printing, compare instead of relying on truthiness. A list or object is truthy, so || never reaches the fallback for an empty one, and rendering the list itself is rejected.
&& guards what follows it. This renders nothing when qty is absent, where the multiplication on its own would be rejected:
{{ data.qty && data.qty * data.rate }}A falsy qty is carried the same way, so a qty of 0 prints 0 rather than nothing.
! always produces a true/false value.
Reading through a missing object
A field may be absent; the object it hangs off may not. On a payload with no account, Papermill rejects {{ data.account.name || 'Acme' }} with message Couldn't find property 'account' in 'data.account.name || 'Acme'', exactly as it rejects a bare {{ data.account.name }}.
Mark the step optional to fall back instead. The ?. goes before the lookup that may fail:
{{ data?.account.name || 'Acme' }}Arithmetic
Combine numbers with +, -, * and /, grouping with parentheses to override precedence.
| Pattern | Example |
|---|---|
| Line total | {{ item.quantity * item.dayRate }} |
| Percent of | {{ item.amount / data.total * 100 }} |
| Grouped | {{ (data.subtotal + data.tax) * data.fxRate }} |
| Negative | {{ -data.adjustment }} |
Operands must be numbers, or text that reads as one. Papermill rejects any other type, division by zero, and a result too large to represent, rather than printing an infinity.
An absent operand is rejected too, which a reference outside arithmetic is not: {{ data.total + 1 }} with no total is rejected with Cannot use nothing in arithmetic in '…', where a bare {{ data.total }} renders empty. A figure derived from a field the payload did not send is a payload to report, not a blank to typeset. Guard the field the figure needs — show-if="data.total" — rather than the figure itself.
Arithmetic computes; it does not format. Wrap the result in <value> for thousands separators and fixed decimals:
xml
<p>Line total: <value separator="true" dp="2">{{ item.quantity * item.dayRate }}</value></p>Hyphens in names
A field name may contain a hyphen — page-number, text-content, and any kebab-case key your payload sends.
Write subtraction with spaces around the -, and read a hyphenated field through its quoted key:
{{ data.total - data.vat }} subtraction
{{ data['total-amount'] }} the field called total-amountAn unspaced - between two names is ambiguous: what it resolves to depends on which fields the payload happens to carry, so renaming a field can change a number instead of reporting a problem. Quote the key.
A name cannot end in a hyphen. {{ data.total- }} is not a field reference, so a field whose name ends in one is readable only as a quoted key.
Reading through a hyphenated name that is absent is rejected, as any missing field along a path is: on a payload with no a-b, {{ data.a-b.c }} is rejected with Couldn't find property 'a-b' in data.a-b.c.
Where Expressions Work
| Context | Example |
|---|---|
| Text content | <p>{{ data.message }}</p> |
| Attribute values | <frame width="{{ data.width }}"> |
| Image sources | <img src="{{ data.photo }}" /> |
| Style values | <span font-color="{{ data.highlight }}"> |
| Component attrs | <my-comp title="{{ data.title }}" /> |
| Page names | <page name="{{ section.data.type }}" /> |
Special Variables
| Variable | Available In | Description |
|---|---|---|
data.* | Everywhere | Top-level document data |
assets.data.* | Everywhere | Asset-defined constant data |
item | Inside <repeat> | Current iteration item (default name) |
attrs.* | Inside components | Component attribute values |
section.data.* | Inside group repeats | Current flow group's data |
visualization.groups | Inside visualisations | Chart data groups |
Outline Entry Properties
Available when iterating over outline collections with <repeat outline="...">:
| Property | Description |
|---|---|
entry['page-number'] | Page where the element appears |
entry['text-content'] | Text content of the element |
entry.index | Position in the collection (1-based) |
entry.id | ID of the outlined element (for target) |