Filters
This page lists every filter, test and function, so you can format numbers, money and dates, translate labels, sum or sort lists, shorten and pad text, and calculate with dates from inside a template. A filter follows a value with | and may take arguments: {{ total | currency("KES") }}. Filters chain from left to right.
Where each filter works
Section titled “Where each filter works”| Filter | Office templates | Text templates | PDF tags and batchName |
|---|---|---|---|
currency, number, date, t |
Yes | Yes | Yes |
image, barcode, link, color, html, chart, pagebreak |
Yes | No | No |
Text, number and date helpers: truncate, unaccent, pad_left, pad_right, avg, ceil, floor, words, date_add, date_sub, start_of, end_of, date_diff, duration |
Yes | Yes | Yes |
| Built-in filters | Yes | Yes | Yes |
currency, number, date and t read the same settings everywhere: the request’s lang, timezone, currency and translations fields, then the template’s {% settings %} and document properties. See Translations and settings.
Using a filter where it does not exist is a 422:
printf '{{ logo | image }}\n' > logo.txtcurl -F template=@logo.txt -F 'data={"logo":"x"}' \ http://localhost:9027/v1/renderThe rich content filters have their own page.
Language
Section titled “Language”Without a language, number, currency and date print as they always have: , between thousands, . before decimals, the currency code in front, and English names. With one, from the request’s lang field or the template’s lang setting, they follow that language’s CLDR data:
lang |
1234.5 | number(2) |
1500 | currency("EUR") |
"2026-01-05" | date("%A %d %B") |
|---|---|---|---|
| none | 1,234.50 |
EUR 1,500.00 |
Monday 05 January |
en-us |
1,234.50 |
€1,500.00 |
Monday 05 January |
fr |
1 234,50 |
1 500,00 € |
lundi 05 janvier |
de |
1.234,50 |
1.500,00 € |
Montag 05 Januar |
de-ch |
1'234.50 |
EUR 1'500.00 |
Montag 05 Januar |
es |
1234,50 |
1500,00 € |
lunes 05 enero |
ja |
1,234.50 |
€1,500.00 |
月曜日 05 1月 |
The spaces are the ones CLDR uses: a narrow no-break space (U+202F) between thousands in French, and a no-break space (U+00A0) before €. Spanish and Polish group only from five digits, so 1234 stays together.
Languages with data: ar, bg, ca, cs, da, de (and de-at, de-ch, de-li), el, en (and en-au, en-ca, en-gb, en-ie, en-in, en-nz, en-us, en-za), es (and es-419, es-mx, es-us), et, eu, fi, fr (and fr-be, fr-ca, fr-ch), gl, he, hi, hr, hu, id, is, it (and it-ch), ja, ko, lt, lv, ms, nb, nl (and nl-be), nn, no, pl, pt (and pt-br, pt-pt), ro, ru, sk, sl, sr, sv (and sv-fi), th, tr, uk, vi, zh (and zh-hk, zh-tw). Another region falls back to its language, so fr-lu formats as fr. A language with no data at all formats as if none were set, though t still uses it.
currency
Section titled “currency”{{ value | currency }}{{ value | currency("KES") }}Without a language, it prints a number with two decimals, commas between thousands, and the code in front.
| Value | Tag | Prints |
|---|---|---|
1234.5 |
currency("USD") |
USD 1,234.50 |
-1234567.891 |
currency("USD") |
-USD 1,234,567.89 |
999.999 |
currency("EUR") |
EUR 1,000.00 |
0.5 |
currency |
0.50 |
2 |
currency("") |
2.00 |
cat > money.txt <<'EOF'{{ a | currency("USD") }}|{{ b | currency }}|{{ c | currency("EUR") }}EOFcurl -F template=@money.txt -F 'data={"a":1234.5,"b":0.5,"c":999.999}' \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = 'USD 1,234.50|0.50|EUR 1,000.00'With a language, the symbol goes where the language puts it, and the amount has the currency’s own decimals: none for JPY, three for KWD, two for most.
lang |
Tag | Prints |
|---|---|---|
en-us |
1500 | currency("USD") |
$1,500.00 |
fr |
1500 | currency("EUR") |
1 500,00 € |
de |
-1500 | currency("EUR") |
-1.500,00 € |
ja |
1500 | currency("JPY") |
¥1,500 |
en |
1500 | currency("KWD") |
KWD 1,500.000 |
printf '{{ 1500 | currency("EUR") }}|{{ 1500 | currency("JPY") }}' > money.txtcurl -F template=@money.txt -F lang=de \ http://localhost:9027/v1/render -o out.txt# The no-break spaces CLDR puts before the symbol, shown here as _.test "$(perl -CSD -pe 's/\x{a0}/_/g' out.txt)" = '1.500,00_€|1.500_¥'- The code is any text you pass. A code CLDR does not know prints as written, with two decimals.
- The code can come from data, as in
{{ total | currency(currency) }}. - With no argument, the request’s
currencyfield applies, then the template’scurrencysetting.currency("")prints no code even when one is set. - The value must be a number. A string such as
"12"is an error:cannot convert string to f64. So isnull.
number
Section titled “number”{{ value | number }}{{ value | number(2) }}Prints a number with separators between thousands: up to three decimals with trailing zeros dropped, or exactly as many as the argument says, from 0 to 20. Halves round away from zero.
| Value | Tag | No language | fr |
de-ch |
|---|---|---|---|---|
1234.5 |
number |
1,234.5 |
1 234,5 |
1'234.5 |
1234.5 |
number(2) |
1,234.50 |
1 234,50 |
1'234.50 |
0.125 |
number(2) |
0.13 |
0,13 |
0.13 |
-1234567.891 |
number(0) |
-1,234,568 |
-1 234 568 |
-1'234'568 |
printf '{{ a | number }}|{{ a | number(2) }}' > n.txtcurl -F template=@n.txt -F 'data={"a":1234.5}' -F lang=de \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = '1.234,5|1.234,50'The value must be a number, as for currency.
{{ issued | date }}{{ issued | date("%d %B %Y") }}The one argument is a format. Without it the format is %Y-%m-%d. A second argument is an error (too many arguments): the time zone comes from the request’s timezone field or the template’s timezone setting.
What it accepts
Section titled “What it accepts”| Value | Treated as |
|---|---|
2026-11-06 |
That date, at midnight |
2026-11-06T10:20 |
Local time, with no conversion |
2026-11-06T10:20:30, 2026-11-06 10:20:30 |
Local time, with no conversion. Fractions of a second are allowed |
2026-11-06T10:20:30Z, 2026-11-06T10:20:30+03:00 |
A moment, shown in the time zone |
2026-11-06 10:20:30+0300 |
A moment, shown in the time zone |
1793980800, 1793980800.5 |
Unix seconds, shown in the time zone |
Anything else, such as 06/11/2026, is refused: date: cannot parse "06/11/2026"; use ISO 8601 or unix seconds.
Format strings
Section titled “Format strings”Formats follow chrono’s strftime syntax. An unknown specifier is an error: date: invalid format "%Q".
| Specifier | Prints, for 6 November 2026 at 10:20:30 |
|---|---|
%Y, %y |
2026, 26 |
%m, %d |
11, 06 |
%B, %b |
November, Nov |
%A, %a |
Friday, Fri |
%p, %P |
AM, am |
%H, %M, %S |
10, 20, 30 |
%j |
310, the day of the year |
With a language, %B, %b, %A, %a, %p and %P print CLDR’s names in it, and everything else stays as it is. In languages where a month changes form inside a date, %B takes the form for a date (5 января in Russian) when the format has a day of the month (%d, %e), and the form for a month alone (январь 2026) when it does not.
printf '{{ d | date("%%A %%d %%B %%Y") }}|{{ d | date("%%B %%Y") }}' > dated.txtcurl -F template=@dated.txt -F 'data={"d":"2026-01-05"}' -F lang=ru \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = 'понедельник 05 января 2026|январь 2026'Time zones
Section titled “Time zones”The time zone is an offset such as +02:00 or -05:00, or an IANA zone such as Europe/Paris or America/New_York, which follows daylight saving. The default is UTC. It changes only values that are a moment: those with a zone, and unix seconds. A value with no zone prints as written.
cat > tz.txt <<'EOF'{{ "2026-03-28T12:00:00Z" | date("%H:%M") }}{{ "2026-03-30T12:00:00Z" | date("%H:%M") }}EOFcurl -F template=@tz.txt -F timezone=Europe/Paris \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = "$(printf '13:00\n14:00')"A zone that is neither is refused: timezone "Mars/Olympus" is neither an offset like +02:00 nor an IANA zone like Europe/Paris. As a request field that is a 400; in a template setting, a 422 when date runs.
The sample invoice uses date in its issue and payment lines:
curl -F template=@invoice.docx -F "data=<invoice.json" \ http://localhost:9027/v1/render -o invoice-filled.docxunzip -p invoice-filled.docx word/document.xml | grep -o 'Issued 06 November 2026'It takes no arguments.
{{ "greeting" | t }}{{ label | t }}It looks the key up in the language, then in the language it falls back to (fr for fr-ca), then in the template’s translations with no language, and prints the key itself when none has it. The request’s translations come first, then the service’s dictionaries, then the template’s own. See Translations and settings.
Built-in filters
Section titled “Built-in filters”These come from minijinja and work in every template. Each example here ran as written.
| Filter | Example | Prints |
|---|---|---|
upper, lower |
{{ "ab" | upper }} |
AB |
title |
{{ "a bc" | title }} |
A Bc |
capitalize |
{{ "a bc" | capitalize }} |
A bc |
trim |
{{ "--x-" | trim("-") }} |
x. With no argument it trims whitespace |
replace |
{{ "a bc" | replace("a", "b") }} |
b bc |
split |
{{ "a bc" | split(" ") | join("+") }} |
a+bc |
lines |
{{ text | lines | length }} |
The number of lines |
indent |
{{ text | indent(2) }} |
Indents every line after the first |
format |
{{ "%s-%s" | format(1, 2) }} |
1-2 |
| Filter | Example | Prints, for l = [1,2,2,3] |
|---|---|---|
length, count |
{{ l | length }} |
4. Works on text and objects too |
first, last |
{{ l | last }} |
3 |
min, max |
{{ l | max }} |
3 |
sum |
{{ l | sum }} |
8 |
join |
{{ l | join(",") }} |
1,2,2,3 |
sort |
{{ l | sort(reverse=true) }} |
[3, 2, 2, 1]. Also takes attribute="price" |
reverse |
{{ l | reverse | list }} |
[3, 2, 2, 1] |
unique |
{{ l | unique | list }} |
[1, 2, 3] |
list |
{{ "ab" | list }} |
['a', 'b'] |
batch |
{{ [1,2,3] | batch(2) | list }} |
[[1, 2], [3]] |
slice |
{{ [1,2,3] | slice(2) | list }} |
[[1, 2], [3]] |
chain |
{{ [1] | chain([2]) | list }} |
[1, 2] |
zip |
{{ [1] | zip(["a"]) | list }} |
[(1, 'a')] |
Lists of objects
Section titled “Lists of objects”These are the ones to use for totals. For o = [{"a":1},{"a":0}]:
| Filter | Example | Prints |
|---|---|---|
sum |
{{ o | sum(attribute="a") }} |
1 |
map |
{{ o | map(attribute="a") | list }} |
[1, 0] |
selectattr |
{{ o | selectattr("a") | list | length }} |
1. Also takes a test: selectattr("a", "eq", 0) |
rejectattr |
{{ o | rejectattr("a") | list | length }} |
1 |
select, reject |
{{ [1,2] | select("odd") | list }} |
[1] |
sort |
{{ o | sort(attribute="a") | map(attribute="a") | list }} |
[0, 1] |
groupby |
{% for g in o | groupby("a") %}{{ g.grouper }}:{{ g.list | length }} {% endfor %} |
0:1 1:1 |
map also applies a filter to each item: {{ ["a"] | map("upper") | list }}.
Numbers and conversion
Section titled “Numbers and conversion”| Filter | Example | Prints |
|---|---|---|
round |
{{ 2.567 | round(1) }} |
2.6 |
abs |
{{ (-2) | abs }} |
2 |
int, float |
{{ "3" | int }} |
3 |
bool |
{{ 1 | bool }} |
True |
string |
{{ 1 | string }} |
1 |
Objects
Section titled “Objects”| Filter | Example | Prints, for m = {"k":"v"} |
|---|---|---|
items |
{{ m | items | list }} |
[('k', 'v')] |
dictsort |
{{ m | dictsort }} |
[('k', 'v')] |
attr |
{{ m | attr("k") }} |
v |
tojson |
{{ m | tojson }} |
{"k": "v"} |
pprint |
{{ m | pprint }} |
The value spread over several lines |
Missing values and escaping
Section titled “Missing values and escaping”| Filter | Example | Does |
|---|---|---|
default, d |
{{ nope | default("x") }} |
Replaces an undefined value. default("x", true) also replaces anything false, including null |
escape, e |
{{ "<" | escape }} |
Prints <. See escaping |
safe |
{{ "<" | safe }} |
Writes a value as it is, with no escaping |
A worked total
Section titled “A worked total”cat > total.txt <<'EOF'{% set total = items | sum(attribute="price") -%}{{ items | map(attribute="name") | join(", ") }}: {{ total | currency("EUR") }}EOFcat > items.json <<'EOF'{ "items": [ {"name": "Lamp", "price": 19.5}, {"name": "Plug", "price": 0.5} ]}EOFcurl -F template=@total.txt -F data=@items.json \ http://localhost:9027/v1/render -o out.txtgrep -x 'Lamp, Plug: EUR 20.00' out.txtFilters that do not exist
Section titled “Filters that do not exist”urlencode, wordcount, striptags, center and nl2br are not in this build. Each gives unknown filter.
Text helpers
Section titled “Text helpers”These work in every template. They count characters, not bytes, so é is one character. A letter written as a base letter and a separate accent mark counts as two.
They take text or a number. null counts as empty text, and a list or an object is an error: expected text or a number.
truncate
Section titled “truncate”{{ title | truncate(40) }}{{ title | truncate(40, end="...") }}{{ title | truncate(40, words=true) }}Shortens text to at most length characters, end included. Text that fits is unchanged. end is … unless you pass another, as the second argument or as end=; end="" cuts with no mark.
With words=true, the cut never falls inside a word: the text stops at the last space that fits. If the first word alone is too long, only end remains.
| Value | Tag | Prints |
|---|---|---|
"crème brûlée au café" |
truncate(9) |
crème br… |
"crème brûlée au café" |
truncate(9, words=true) |
crème… |
"crème brûlée au café" |
truncate(13, "...", words=true) |
crème... |
"crème brûlée au café" |
truncate(30) |
crème brûlée au café |
A negative length is an error.
unaccent
Section titled “unaccent”{{ name | unaccent }}Removes accents and other marks above and below letters: crème brûlée gives creme brulee, and Łódź gives Lodz. Letters that are not a base letter plus a mark are spelled with plain letters: ß gives ss, æ gives ae, ø gives o. Scripts whose marks change the letter, such as Devanagari vowel signs or Japanese kana, are left as they are.
Use it for file names, identifiers and systems that accept only ASCII, and chain it: {{ name | unaccent | lower | replace(" ", "-") }}.
pad_left and pad_right
Section titled “pad_left and pad_right”{{ number | pad_left(6, "0") }}{{ name | pad_right(20) }}{{ name | pad_right(20, char=".") }}Fill text out to width characters, with spaces or the one character you pass. pad_left adds the fill in front, which right-aligns; pad_right adds it after. Text already that long is unchanged, never cut: chain truncate first for a fixed width.
| Value | Tag | Prints |
|---|---|---|
42 |
pad_left(6, "0") |
000042 |
"é" |
pad_right(3, ".") |
é.. |
"abcdef" |
pad_left(3) |
abcdef |
The width must be between 0 and 10,000, and the fill exactly one character. Anything else is an error, such as pad_left: the width must be between 0 and 10000, got 100000.
cat > text.txt <<'EOF'[{{ t | truncate(9) }}][{{ t | truncate(9, words=true) }}][{{ t | unaccent }}][{{ n | pad_left(6, "0") }}][{{ "é" | pad_right(3, ".") }}]EOFcurl -F template=@text.txt -F 'data={"t":"crème brûlée au café","n":42}' \ http://localhost:9027/v1/render -o out.txtcat > expected.txt <<'EOF'[crème br…][crème…][creme brulee au cafe][000042][é..]EOFtest "$(cat out.txt)" = "$(cat expected.txt)"Number helpers
Section titled “Number helpers”These work in every template. Calculations shows them alongside the built-in arithmetic.
{{ scores | avg }}{{ lines | avg(attribute="price") }}The mean of a list of numbers, or of one attribute of a list of objects. The attribute can be a path such as "totals.net". Items whose value is missing or null are left out of the count. An empty list, or one with no values, gives null; add | default(0, true) to print 0 instead. A value that is not a number is an error: avg: can only average numbers, got string.
A whole result prints without decimals: {{ [1, 2, 6] | avg }} prints 3, and {{ [1, 2] | avg }} prints 1.5.
ceil and floor
Section titled “ceil and floor”{{ 2.1 | ceil }}{{ 2.9 | floor }}The next whole number up or down, as a whole number: 3 and 2, not 3.0. For negative numbers, up is toward zero: {{ -2.1 | ceil }} is -2. A value that is not a number is an error. To round to decimals instead, use round(2, "ceil").
{{ 1234 | words }}{{ 21 | words(ordinal=true) }}{{ 1250.5 | words(currency="EUR") }}{{ 1234 | words(lang="fr") }}Spells a number out.
| Value | Tag | Prints |
|---|---|---|
1234 |
words |
one thousand two hundred thirty-four |
-7 |
words |
minus seven |
1.5 |
words |
one point five |
21 |
words(ordinal=true) |
twenty-first |
1250.5 |
words(currency="EUR") |
one thousand two hundred fifty euros and fifty cents |
1 |
words(currency="USD") |
one US dollar |
81 |
words(lang="fr") |
quatre-vingt-un |
70 |
words(lang="fr-BE") |
septante |
- Languages. English (
en), French (fr, withfr-BEandfr-CHfor Belgian and Swiss numbers) and Ukrainian (uk). Withoutlang=, the template’slangsetting applies, and a language with no words, or no setting, gives English.lang=with a language that has no words is an error:words: no words for language "de"; use en, fr or uk. - Currencies.
currency=takes an ISO 4217 code, in any case. The codes with words are AED, ARS, AUD, BRL, CAD, CHF, CLP, CNY, COP, CRC, DZD, EUR, GBP, HKD, IDR, ILS, INR, JPY, KRW, KWD, KZT, MXN, MYR, NOK, NZD, PEN, PHP, PLN, QAR, RUB, SAR, SGD, THB, TRY, TWD, UAH, USD, UYU, VND and ZAR, plus the genericDOLLAR,DINAR,PESOandRIYAL. The amount is rounded to cents first. Another code is an error; for a currency with no words, useDOLLARandreplace, as the worked invoice does. - Limits. Numbers up to 999,999,999,999,999 either side of zero. A larger one is refused:
words: 1e15 is too large. An ordinal must be a whole number of 0 or more, andordinalandcurrencycannot be combined. - The value must be a number; text such as
"12"is an error. Convert it with| intor| float.
cat > words.txt <<'EOF'{{ 1234 | words }}{{ 21 | words(ordinal=true) }}{{ 1250.5 | words(currency="EUR") }}{{ 81 | words(lang="fr") }}EOFcurl -F template=@words.txt -F 'data={}' \ http://localhost:9027/v1/render -o out.txtcat > expected.txt <<'EOF'one thousand two hundred thirty-fourtwenty-firstone thousand two hundred fifty euros and fifty centsquatre-vingt-unEOFtest "$(cat out.txt)" = "$(cat expected.txt)"Date helpers
Section titled “Date helpers”These work in every template. They read dates as date does, ISO 8601 text or unix seconds, and return ISO 8601 text, which date can print in any format. Calculations explains each rule with examples.
| Filter | Does | Example | Gives |
|---|---|---|---|
date_add(amount, unit) |
Adds whole units | "2028-01-31" | date_add(1, "month") |
2028-02-29 |
date_sub(amount, unit) |
Subtracts whole units | "2028-03-31" | date_sub(1, "month") |
2028-02-29 |
start_of(unit) |
The first second of the period | "2026-11-06T10:20:00Z" | start_of("day") |
2026-11-06T00:00:00Z |
end_of(unit) |
The last second of the period | "2026-11-06" | end_of("month") |
2026-11-30 |
date_diff(other, unit) |
Whole units from other to the value |
"2026-12-25" | date_diff("2026-11-06", "weeks") |
7 |
duration(unit) |
An interval in words or in a unit | 5400000 | duration |
2 hours |
- The units are
year,quarter,month,week,day,hour,minuteandsecond, singular or plural.date_add,date_subanddate_diffcount days when the unit is left out. - Adding months keeps the day of the month, or uses the month’s last day when the target month is shorter.
- A date stays a date, a time with no zone stays one, and a time with a zone keeps its offset. Unix seconds come back as UTC.
- A week starts on Monday.
date_diffis negative when the value is beforeother, and drops part of a unit.- A result out of range is an error,
date_add: the date is out of range, and so is an unknown unit.
cat > dates.txt <<'EOF'{{ d | date_add(1, "month") }}{{ d | end_of("month") }}{{ "2026-12-25" | date_diff("2026-11-06", "weeks") }}{{ 5400000 | duration }}EOFcurl -F template=@dates.txt -F 'data={"d":"2028-01-31"}' \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = "$(printf '2028-02-29\n2028-01-31\n7\n2 hours')"duration
Section titled “duration”{{ elapsed_ms | duration }}{{ elapsed_ms | duration("hours") }}{{ "P1DT12H" | duration("hours") }}Takes milliseconds, or an ISO 8601 duration such as P1Y2M3DT4H5M6S or PT1H30M. With no argument, or "human", it prints words: a few seconds, a minute, 5 minutes, an hour, 3 hours, a day, 12 days, a month, 4 months, a year, 2 years. The words are in English and ignore the sign.
With a unit, years, months, weeks, days, hours, minutes, seconds or milliseconds (ms), it prints a number, with decimals when needed: 5400000 | duration("hours") is 1.5. A month counts 30 days and a year 365. Text that is not an ISO duration, such as "2 hours", is an error.
is asks a question about a value: {% if qty is odd %}. The tests available are defined, undefined, none, empty, boolean, number, integer (or int), float, string, sequence, iterable, mapping, odd, even, divisibleby, startingwith, endingwith, lower, upper, in, true, false, safe, sameas, eq, ne, lt, le, gt and ge.
empty works in every template. It is true for null, a missing value, "", [] and {}, and false for 0 and false: {% if notes is not empty %}. Conditions explains each test.
Functions
Section titled “Functions”range(3) gives [0, 1, 2], dict(a=1) gives an object, and namespace(x=1) gives an object a loop can change with {% set ns.x = ... %}.
now() gives the time of the render in UTC, to the second, such as 2026-11-06T10:20:00Z, in every template. Send ctx={"now":"2026-11-06T10:20:00Z"} with the request to fix it, so the output is the same on every run:
printf '{{ now() }}|{{ now() | date_add(30) }}' > now.txtcurl -F template=@now.txt -F 'data={}' \ -F 'ctx={"now":"2026-11-06T10:20:00Z"}' \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = '2026-11-06T10:20:00Z|2026-12-06T10:20:00Z'