Liquid filters
A filter changes a value on its way into the file: {{ value | filter }}, or {{ value | filter: argument }}. Filters chain left to right.
The engine Selenic Orders uses has fewer filters than Shopify. Those listed here work; the ones that do not are listed at the end of this page. A filter that does not exist is an error, and Preview Sample shows it.
Every example here is rendered by the app’s own Preview Sample in a test, and the box under each one is exactly what it produces. The sample customer is The Anchor Hotel & Bar, Ltd, whose contact is Niamh O’Brien: a comma, an ampersand and an apostrophe, which is what the escaping filters are for.
The escaping filters
Your product and account data holds names like Ben & Jerry's 500ml "Original". Written into a file unescaped, a quote ends a CSV field early, an ampersand makes XML invalid, and a quote breaks JSON. Apply the filter for the format you are writing. None of them adds the surrounding quotes: your template does that.
| Filter | For | Does |
|---|---|---|
csv_escape | CSV | Doubles " to "" |
xml_escape | XML | Escapes &, <, >, " and ' |
json_escape | JSON | Escapes quotes, backslashes and control characters |
All three write nothing for a missing value, rather than failing.
csv_escape
{% for customer in customers %}"{{ customer.companyName | csv_escape }}","{{ '12" pizza tray' | csv_escape }}"{% endfor %}"The Anchor Hotel & Bar, Ltd","12"" pizza tray"The comma in the company name needs no escaping, because the field is in quotes.
xml_escape
{% for customer in customers %}<Name>{{ customer.companyName | xml_escape }}</Name><Contact>{{ customer.contactName | xml_escape }}</Contact>{% endfor %}<Name>The Anchor Hotel & Bar, Ltd</Name><Contact>Niamh O'Brien</Contact>json_escape
{% for customer in customers %}{"company": "{{ customer.companyName | json_escape }}", "note": "{{ 'Say "when"' | json_escape }}"}{% endfor %}{"company": "The Anchor Hotel & Bar, Ltd", "note": "Say \"when\""}Standard filters
default
The value, or the argument when the value is empty or missing.
{% for order in orders %}{{ order.orderTaxCode | default: "STD" }}|{{ order.appliedPromotionCode | default: "NONE" }}|{{ order.carriageWaivedBy | default: "NONE" }}{% endfor %}T1|SUMMER10|NONEGive it several arguments and it takes the first that is not empty, which can be another property:
{% for order in orders %}{{ order.carriageWaivedBy | default: order.appliedPromotionCode, "NONE" }}{% endfor %}SUMMER10default_if_none
Like default, but only for a property that is missing altogether. Empty text stays empty.
{% for order in orders %}[{{ order.carriageWaivedBy | default_if_none: "NONE" }}][{{ order.notAField | default_if_none: "NONE" }}]{% endfor %}[][NONE]size
How many entries a list has. It also works inside an if.
{% for order in orders %}{{ order.lineItems | size }} lines{% if order.lineItems | size > 1 %}, more than one{% endif %}{% endfor %}2 lines, more than oneOn text, size is always 0. It counts lists, not characters:
{% for order in orders %}{{ order.orderId | size }}{% endfor %}0upcase and downcase
upper and lower are other names for the same two.
{% for customer in customers %}{{ customer.accountStatus | upcase }} {{ customer.priceListId | downcase }} {{ customer.currencyCode | lower }} {{ customer.taxCode | upper }}{% endfor %}ACTIVE trade eur EXPORTcapitalize
Makes the first letter a capital and leaves the rest as it is. capfirst is another name for it.
{{ "next day" | capitalize }} / {{ "nEXT DAY" | capfirst }}Next day / NEXT DAYjoin
Joins a list of text with the argument between entries. The only list of plain text in the export data is a customer’s tags, and Preview Sample’s customer has no tags, so Preview reports an error for this where a real export works:
{% for customer in customers %}{{ customer.tags | join: ", " }}{% endfor %}For a customer tagged wholesale and north, a real export writes wholesale, north.
Dates: date
date writes a date in the layout you give it, using the same % codes as Shopify and most programming languages. Dates in an export are 2026-08-09 14:32:00 until you change them.
{% for order in orders %}{{ order.dateCreated | date: "%d/%m/%Y" }}{% endfor %}09/08/2026{% for order in orders %}{{ order.dateCreated | date: "%A %-d %B %Y, %H:%M" }}{% endfor %}Sunday 9 August 2026, 14:32| Code | Gives | Sample |
|---|---|---|
%d | Day of the month, two digits | 09 |
%m | Month, two digits | 08 |
%Y | Year | 2026 |
%y | Year, two digits | 26 |
%H | Hour, 24-hour clock | 14 |
%I | Hour, 12-hour clock | 02 |
%M | Minute | 32 |
%S | Second | 00 |
%p | AM or PM | PM |
%A | Day name | Sunday |
%a | Day name, short | Sun |
%B | Month name | August |
%b | Month name, short | Aug |
%j | Day of the year, three digits | 221 |
%e | Day of the month, space-padded | 9 |
%% | A percent sign | % |
A hyphen after the % drops the leading zero: %-d is 9, %-m is 8. Day and month names are in English.
A blank date, such as requestedDate when no delivery date was asked for, stays blank. Text that is not a date is written unchanged. date without a layout is an error.
Arithmetic: plus, minus, times, divided_by
Each takes one number, which can be another property.
{% for order in orders %}{{ order.lineItems.0.qty | plus: 1 }} {{ order.lineItems.0.qty | minus: 10 }} {{ order.lineItems.0.qty | times: 2 }}{% endfor %}7 -4 12{% for order in orders %}{{ order.subtotal | minus: order.promotionDiscount | round: 2 }}{% endfor %}159.60divided_by a whole number gives a whole number, rounded down, as in Shopify. Divide by 2.0 rather than 2 to keep the fraction:
{{ 7 | divided_by: 2 }} {{ 7 | divided_by: 2.0 }}3 3.5A blank or missing value counts as 0, so an order with no figure in a field still exports. Text that is not a number is an error, and so is dividing by 0.
Rounding: round
round on its own rounds to a whole number. round: 2 rounds to two decimal places and always writes both of them, which is how an ERP expects money (Shopify would write 168.0):
{% for order in orders %}{{ order.subtotal | round: 2 }} {{ order.grandTotal | round }} {{ order.lineItems.0.tax_rate | times: 100 | round }}%{% endfor %}168.00 196 20%An exact half rounds away from zero (1.005 to 1.01), the same rule the app uses for every order’s own figures, so a sum worked out in a template agrees with the order. Round last: a result of round: 2 can still go into more arithmetic, but it cannot be compared with < or >.
Cutting text: slice
slice: start, length takes part of a piece of text. Positions count from 0, and a negative start counts back from the end. Without a length it takes one character.
{% for order in orders %}{{ order.orderId | slice: 4, 4 }} {{ order.orderId | slice: -4, 4 }} {{ order.lineItems.0.title | slice: 0, 9 }}{% endfor %}0001 0001 GlenvallePast the end it stops short rather than failing, so slice: 0, 30 is a safe way to keep a description within a fixed-width column.
On a list, slice picks entries. This is the way to skip or stop a loop, since limit and offset are ignored:
{% for order in orders %}{% for item in order.lineItems | slice: 1, 1 %}{{ item.sku }}{% endfor %}{% endfor %}GN002A missing value and upcase
upcase, downcase and capitalize fail on a value that is not there at all, and a customer created on a tablet often has no contact name, phone number or website. Preview’s sample customer has empty text in those places, so a template can pass Preview and fail on a real customer. Put default: "" first:
{% for customer in customers %}[{{ customer.notAField | default: "" | upcase }}]{% endfor %}[]{% for order in orders %}{{ order.notAField | upcase }}{% endfor %}That one is an error.
Chaining
Filters apply left to right, so escape last:
{% for order in orders %}{{ order.customerCompanyNameSnapshot | upcase | csv_escape }}{% endfor %}KESTREL WINE BARFilters this engine does not have
Each of these is an error in Preview Sample and in a real export. The list is checked against the app.
abs, append, at_least, at_most, ceil, compact, concat, escape, first, floor, last, lstrip, map, modulo, money, newline_to_br, prepend, remove, replace, reverse, rstrip, sort, split, strip, strip_html, sum, truncate, truncatewords, uniq, url_encode, where.
What that means in practice:
- No joining text with a filter. Put the pieces side by side instead:
{{ order.orderId }}-{{ order.customerId }}, or build it withcapture(see Liquid tags). - No
floor,ceilorabs.roundis the only rounding. - No
truncate.slice: 0, 30cuts text to 30 characters, without adding an ellipsis.