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 |
cart.items, cart.value, cart.subtotal, cart.currency, cart.checkoutUrl | The contact's synced store cart - items with name, quantity, price, total, imageUrl, url, sku The cart's value is the total the store sent (it can include shipping); subtotal is the items subtotal, before shipping and tax. | Products |
last_order.items, last_order.total, last_order.currency, last_order.placedAt, last_order.orderUrl | Their most recent order that was not cancelled or refunded - the same item fields as the cart | Products |
top_products | The products they order most often - up to 5 of name, sku, imageUrl, url, timesOrdered, lastOrderedAt, most frequent first | Products |
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.
cart, last_order and top_products are empty for a contact with no store orders - wrap them in {% if %}.
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) |
landing_page_url | A link to a published landing page by its slug. Carries the recipient's token, so the page knows who arrived; an optional argument appends a query string. The link is always built; a slug that is not published (or does not exist) answers 404 at the redirect. Empty only when the slug is malformed or the render has no workspace | {{ "spring-sale" | landing_page_url: "utm_campaign=spring" }} → https://api-eu1.joryio.com/api/p/go/<workspace>/spring-sale?jp=…&utm_campaign=spring |
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. Formats in the workspace time zone in previews and sends alike; a date-only value such as a birthday (1990-05-01) is never shifted. A strftime pattern also works ("%d %b %H:%M"), and an optional second argument names another IANA zone - an unknown zone falls back to the workspace zone | {{ order.createdAt | date_format }} → Jul 12, 2026 · {{ order.createdAt | date_format: "long" }} → July 12, 2026 · "full" → Sunday, July 12, 2026 · {{ order.createdAt | date_format: "%d %b %H:%M", "America/New_York" }} → 12 Jul 05:30 |
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 |
Dates and times in message content are shown in the workspace time zone, the same zone the rest of the app uses. This applies to date_format and to the standard date filter ({{ order.createdAt | date: "%H:%M" }}); pass a zone as the last argument to use another one.
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 - about 5 minutes in the editor preview, about a minute on a send - 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. - All three filters (
products,entityand thecatalogalias) run on every render path with the same names: the editor preview, campaign sends, journey sends, and every channel worker. What the preview shows is what the recipient gets. - 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.
Recommendations
recommendations.<slot> is an array of recommended products for the recipient, from a
recommendation model. The slot is the model name as a key
(For you becomes for_you); recommendations.default is the workspace default model. Each item
has name, price (formatted), compareAtPrice, discountPercent, url, image, description
and id. It works in every channel - email (campaigns and journeys), SMS, WhatsApp, Viber, push, in-app
and landing pages - and in editor previews and test sends:
{% for item in recommendations.for_you limit: 3 %}
{{ item.name }} - {{ item.price }} {{ item.url }}
{% endfor %}
See Liquid slots for slot naming and item fields, and the block list for products that are never recommended.
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 - Product recommendations - models, types and the
recommendations.*slots