Liquid Reference
Joryio personalizes messages with Liquid - you write
placeholders like {{ user.firstName }} and filters like {{ price | currency }}, and
they resolve per recipient at send time. This page is the complete reference: every
variable namespace and every Joryio custom filter, with examples.
New to Liquid in Joryio? Start with Liquid Templates for the basics, then come back here when you need the details.
All filters below are registered on one shared Liquid engine used by every render path, so the same template renders identically in email, SMS, WhatsApp, push, in-app, and webhook templates - in campaigns, journeys, and previews alike. What you see in preview is what every channel sends.
Two forgiving defaults worth knowing:
- A variable with no value renders as empty text - never an error.
- An unknown filter passes the value through unchanged rather than failing.
Variables
The available namespaces, at a glance. Follow the links for the full details of each.
| Namespace | What it holds | Details |
|---|---|---|
user.firstName, user.lastName, user.email, user.phone, user.id, user.externalId, user.whatsappName | The contact's default attributes | Message Variables |
user.custom.<attribute> | Any custom attribute on the contact (e.g. user.custom.plan) | Message Variables |
trigger.properties.<field> | How they entered the journey - the entry event, fixed for the whole run | Message Variables |
event.properties.<field>, event.name | The latest thing they did - the most recent event that advanced the journey | Message Variables |
reply.text, reply.type, reply.profile.name | Friendly aliases for the contact's latest inbound WhatsApp or SMS reply | Message Variables |
blocks.<slug> | A reusable Content Block, rendered inline | Content Blocks |
unsubscribe_url, preferences_url, resubscribe_url | Per-recipient subscription links (email, SMS, WhatsApp session) | Liquid Templates |
products and entity filters | Product-catalog or custom-entity records from a saved Selection | Catalog feeds below |
trigger.*, event.*, and reply.* resolve only inside a journey; the rest work
everywhere.
Filters
Chain filters with |, pass arguments after :. For example:
{{ user.firstName | capitalize | default: "there" }}
{{ order.total | currency: "EUR" }}
Text
| Filter | What it does | Example → Output |
|---|---|---|
capitalize | Uppercases the first letter and lowercases the rest | {{ "mAYA" | capitalize }} → Maya |
uppercase | Converts the whole string to uppercase | {{ "sale" | uppercase }} → SALE |
lowercase | Converts the whole string to lowercase | {{ "SALE" | lowercase }} → sale |
truncate | Cuts a string to a length (default 50) and appends a suffix (default ...). The suffix is added after the cut, on top of the length | {{ "The quick brown fox jumps" | truncate: 9 }} → The quick... |
strip_html | Removes HTML tags | {{ "<b>Sale</b> today" | strip_html }} → Sale today |
url_encode | URL-encodes a string (for building links) | {{ "red shoes" | url_encode }} → red%20shoes |
pluralize | Given a number, returns the singular or plural word. Plural defaults to singular + s; pass a third argument for irregular plurals | 3 {{ 3 | pluralize: "item" }} → 3 items |
default | Fallback when the value is null, undefined, or an empty string | {{ user.firstName | default: "there" }} → there (when empty) |
Numbers & money
| Filter | What it does | Example → Output |
|---|---|---|
currency | Formats a number as money. Currency code defaults to USD; pass any ISO code. Uses en-US formatting (symbol first, comma thousands) | {{ 1249.5 | currency }} → $1,249.50 · {{ order.total | currency: "EUR" }} → €49.90 |
Dates
| Filter | What it does | Example → Output |
|---|---|---|
date_format | Formats a date. Styles: short (default), long, full. An unknown style falls back to short. English (en-US) month/day names | {{ order.createdAt | date_format }} → Jul 12, 2026 · {{ order.createdAt | date_format: "long" }} → July 12, 2026 · "full" → Sunday, July 12, 2026 |
add_days | Adds N days to a date (negative to subtract). Returns an ISO timestamp - chain date_format to make it readable | {{ order.createdAt | add_days: 7 | date_format }} → Jul 19, 2026 |
time_ago | Human-friendly relative time (year/month/week/day/hour/minute/second granularity) | {{ user.custom.lastOrderAt | time_ago }} → 3 days ago |
Arrays
| Filter | What it does | Example → Output |
|---|---|---|
join | Joins array items into a string. Separator defaults to , | {{ names | join }} → Ana, Ben, Gal · {{ names | join: " / " }} → Ana / Ben / Gal |
map | Extracts one property from each item, returning a new array | {{ items | map: "name" | join }} → Mug, Tee |
first | The first item | {{ items | first }} |
last | The last item | {{ items | last }} |
size | Length of an array or a string (0 for anything else) | {{ cart.items | size }} → 3 |
count | Number of items in an array (0 if not an array) | {{ cart.items | count }} → 3 |
Aggregates & filtering
These operate on arrays of records - cart items, orders, or catalog selections. Every
field argument supports dot-paths into nested objects (e.g. "price.amount").
| Filter | What it does | Example → Output |
|---|---|---|
sum | Sums a numeric field across the array (missing values count as 0) | {{ orders | sum: "totalAmount" }} → 540 |
avg | Averages a numeric field (0 for an empty array) | {{ orders | avg: "totalAmount" }} → 180 |
max | Largest numeric value of a field (non-numeric values ignored; 0 if none) | {{ products | max: "price" }} → 129.9 |
min | Smallest numeric value of a field | {{ products | min: "price" }} → 19.9 |
where | Filters the array. Three call shapes: where: "featured" keeps truthy items; where: "category", "shoes" keeps equal items; where: "price", "gt", 100 compares with an operator: eq, neq, gt, gte, lt, lte, contains, in (symbol forms ==, !=, >, >=, <, <= also work) | {{ items | where: "category", "shoes" | count }} → 2 |
sort | Sorts by a field, order asc (default) or desc. With no field, sorts the values themselves | {{ products | sort: "price", "desc" | first }} → the most expensive product |
A worked example - the recipient's three most recent orders:
{% assign recent = orders | sort: 'createdAt', 'desc' %}
{% for order in recent limit: 3 %}
- {{ order.createdAt | date_format }}: {{ order.totalAmount | currency }}
{% endfor %}
Total spent: {{ orders | sum: 'totalAmount' | currency }}
Catalog feeds
Two filters pull collections of records into a message, each backed by a saved
Selection scoped to your workspace. Both return an array - use them in an assign tag,
then loop.
products - store product catalog
Pulls from your synced product catalog (Shopify / WooCommerce / Magento) via a product Selection. There is one product catalog, so the argument is the Selection name:
{% assign products = 'featured' | products %}
{% for item in products %}
- {{ item.name }}: {{ item.price | currency }}
{% endfor %}
entity - custom entity feed
Pulls records from a Custom Entity. The first argument is the entity name, the second (optional) is the Selection name:
{% assign episodes = 'tv_series' | entity: 'latest' %}
{% for item in episodes %}
- {{ item.name }}
{% endfor %}
You can pass variables into a parameterized entity Selection - for example, feed it the journey's entry event:
{% assign items = 'products' | entity: 'product_by_id', trigger %}
Notes on how these behave:
- Selections that are the same for every recipient (no user-attribute or context filters) are cached for about 5 minutes, so bulk sends don't re-query per recipient. Personalized entity selections are evaluated fresh for each recipient.
- If the entity/product Selection can't be found (or the query fails), the filter returns an
empty array - your
forloop simply renders nothing, never an error. - The personalization picker's Product catalog and Custom entity options build the
assignsnippet for you (see Message Variables).
catalogThe older catalog filter is a deprecated alias of entity and still works, so existing
templates don't break. Prefer entity for custom entities and products for the store
catalog going forward.
Where Joryio differs from standard Liquid
A few filters intentionally behave differently from their Shopify/LiquidJS namesakes:
capitalizealso lowercases the remainder of the string ("mAYA"becomesMaya, notMAYA).defaulttreats an empty string as missing, so{{ user.firstName | default: "there" }}falls back even when the attribute exists but is blank.truncateappends the suffix after the cut length (standard Liquid counts the ellipsis inside the length).whereandsortare supersets of the built-ins: they accept the standard call shapes and add comparison operators / sort direction, plus dot-path fields.
Standard Liquid still works
Everything above is on top of standard LiquidJS: the built-in
tags - {% if %} / {% elsif %} / {% else %}, {% for %}, {% assign %},
{% case %} and friends - and the built-in filters (upcase, downcase, date,
replace, split, plus, times, and many more) all work in any Joryio template.
Where a Joryio filter shares a name with a built-in (capitalize, default, where,
sort, join, map, first, last, size), the Joryio version described on this page
is the one that runs. See the full built-in list at
liquidjs.com/filters/overview.html.
Related
- Liquid Templates - intro and the Personalize picker
- Message Variables -
trigger.*,event.*, andreply.*in journeys - Content Blocks - reusable snippets via
blocks.slug