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.

FilterForDoes
csv_escapeCSVDoubles " to ""
xml_escapeXMLEscapes &, <, >, " and '
json_escapeJSONEscapes quotes, backslashes and control characters

All three write nothing for a missing value, rather than failing.

csv_escape

Liquid
{% for customer in customers %}"{{ customer.companyName | csv_escape }}","{{ '12" pizza tray' | csv_escape }}"{% endfor %}
Text
"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

Liquid
{% for customer in customers %}<Name>{{ customer.companyName | xml_escape }}</Name><Contact>{{ customer.contactName | xml_escape }}</Contact>{% endfor %}
Text
<Name>The Anchor Hotel &amp; Bar, Ltd</Name><Contact>Niamh O&apos;Brien</Contact>

json_escape

Liquid
{% for customer in customers %}{"company": "{{ customer.companyName | json_escape }}", "note": "{{ 'Say "when"' | json_escape }}"}{% endfor %}
Text
{"company": "The Anchor Hotel & Bar, Ltd", "note": "Say \"when\""}

Standard filters

default

The value, or the argument when the value is empty or missing.

Liquid
{% for order in orders %}{{ order.orderTaxCode | default: "STD" }}|{{ order.appliedPromotionCode | default: "NONE" }}|{{ order.carriageWaivedBy | default: "NONE" }}{% endfor %}
Text
T1|SUMMER10|NONE

Give it several arguments and it takes the first that is not empty, which can be another property:

Liquid
{% for order in orders %}{{ order.carriageWaivedBy | default: order.appliedPromotionCode, "NONE" }}{% endfor %}
Text
SUMMER10

default_if_none

Like default, but only for a property that is missing altogether. Empty text stays empty.

Liquid
{% for order in orders %}[{{ order.carriageWaivedBy | default_if_none: "NONE" }}][{{ order.notAField | default_if_none: "NONE" }}]{% endfor %}
Text
[][NONE]

size

How many entries a list has. It also works inside an if.

Liquid
{% for order in orders %}{{ order.lineItems | size }} lines{% if order.lineItems | size > 1 %}, more than one{% endif %}{% endfor %}
Text
2 lines, more than one

On text, size is always 0. It counts lists, not characters:

Liquid
{% for order in orders %}{{ order.orderId | size }}{% endfor %}
Text
0

upcase and downcase

upper and lower are other names for the same two.

Liquid
{% for customer in customers %}{{ customer.accountStatus | upcase }} {{ customer.priceListId | downcase }} {{ customer.currencyCode | lower }} {{ customer.taxCode | upper }}{% endfor %}
Text
ACTIVE trade eur EXPORT

capitalize

Makes the first letter a capital and leaves the rest as it is. capfirst is another name for it.

Liquid
{{ "next day" | capitalize }} / {{ "nEXT DAY" | capfirst }}
Text
Next day / NEXT DAY

join

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:

Liquid
{% 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.

Liquid
{% for order in orders %}{{ order.dateCreated | date: "%d/%m/%Y" }}{% endfor %}
Text
09/08/2026
Liquid
{% for order in orders %}{{ order.dateCreated | date: "%A %-d %B %Y, %H:%M" }}{% endfor %}
Text
Sunday 9 August 2026, 14:32
CodeGivesSample
%dDay of the month, two digits09
%mMonth, two digits08
%YYear2026
%yYear, two digits26
%HHour, 24-hour clock14
%IHour, 12-hour clock02
%MMinute32
%SSecond00
%pAM or PMPM
%ADay nameSunday
%aDay name, shortSun
%BMonth nameAugust
%bMonth name, shortAug
%jDay of the year, three digits221
%eDay 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.

Liquid
{% for order in orders %}{{ order.lineItems.0.qty | plus: 1 }} {{ order.lineItems.0.qty | minus: 10 }} {{ order.lineItems.0.qty | times: 2 }}{% endfor %}
Text
7 -4 12
Liquid
{% for order in orders %}{{ order.subtotal | minus: order.promotionDiscount | round: 2 }}{% endfor %}
Text
159.60

divided_by a whole number gives a whole number, rounded down, as in Shopify. Divide by 2.0 rather than 2 to keep the fraction:

Liquid
{{ 7 | divided_by: 2 }} {{ 7 | divided_by: 2.0 }}
Text
3 3.5

A 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):

Liquid
{% for order in orders %}{{ order.subtotal | round: 2 }} {{ order.grandTotal | round }} {{ order.lineItems.0.tax_rate | times: 100 | round }}%{% endfor %}
Text
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.

Liquid
{% for order in orders %}{{ order.orderId | slice: 4, 4 }} {{ order.orderId | slice: -4, 4 }} {{ order.lineItems.0.title | slice: 0, 9 }}{% endfor %}
Text
0001 0001 Glenvalle

Past 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:

Liquid
{% for order in orders %}{% for item in order.lineItems | slice: 1, 1 %}{{ item.sku }}{% endfor %}{% endfor %}
Text
GN002

A 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:

Liquid
{% for customer in customers %}[{{ customer.notAField | default: "" | upcase }}]{% endfor %}
Text
[]
Liquid
{% for order in orders %}{{ order.notAField | upcase }}{% endfor %}

That one is an error.

Chaining

Filters apply left to right, so escape last:

Liquid
{% for order in orders %}{{ order.customerCompanyNameSnapshot | upcase | csv_escape }}{% endfor %}
Text
KESTREL WINE BAR

Filters 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 with capture (see Liquid tags).
  • No floor, ceil or abs. round is the only rounding.
  • No truncate. slice: 0, 30 cuts text to 30 characters, without adding an ellipsis.