Skip to main content

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.

NamespaceWhat it holdsDetails
user.firstName, user.lastName, user.email, user.phone, user.id, user.externalId, user.whatsappNameThe contact's default attributesMessage 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 runMessage Variables
event.properties.<field>, event.nameThe latest thing they did - the most recent event that advanced the journeyMessage Variables
reply.text, reply.type, reply.profile.nameFriendly aliases for the contact's latest inbound WhatsApp or SMS replyMessage Variables
blocks.<slug>A reusable Content Block, rendered inlineContent Blocks
unsubscribe_url, preferences_url, resubscribe_urlPer-recipient subscription links (email, SMS, WhatsApp session)Liquid Templates
products and entity filtersProduct-catalog or custom-entity records from a saved SelectionCatalog 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

FilterWhat it doesExample → Output
capitalizeUppercases the first letter and lowercases the rest{{ "mAYA" | capitalize }}Maya
uppercaseConverts the whole string to uppercase{{ "sale" | uppercase }}SALE
lowercaseConverts the whole string to lowercase{{ "SALE" | lowercase }}sale
truncateCuts 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_htmlRemoves HTML tags{{ "<b>Sale</b> today" | strip_html }}Sale today
url_encodeURL-encodes a string (for building links){{ "red shoes" | url_encode }}red%20shoes
pluralizeGiven a number, returns the singular or plural word. Plural defaults to singular + s; pass a third argument for irregular plurals3 {{ 3 | pluralize: "item" }}3 items
defaultFallback when the value is null, undefined, or an empty string{{ user.firstName | default: "there" }}there (when empty)

Numbers & money

FilterWhat it doesExample → Output
currencyFormats 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

FilterWhat it doesExample → Output
date_formatFormats 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_daysAdds 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_agoHuman-friendly relative time (year/month/week/day/hour/minute/second granularity){{ user.custom.lastOrderAt | time_ago }}3 days ago

Arrays

FilterWhat it doesExample → Output
joinJoins array items into a string. Separator defaults to , {{ names | join }}Ana, Ben, Gal · {{ names | join: " / " }}Ana / Ben / Gal
mapExtracts one property from each item, returning a new array{{ items | map: "name" | join }}Mug, Tee
firstThe first item{{ items | first }}
lastThe last item{{ items | last }}
sizeLength of an array or a string (0 for anything else){{ cart.items | size }}3
countNumber 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").

FilterWhat it doesExample → Output
sumSums a numeric field across the array (missing values count as 0){{ orders | sum: "totalAmount" }}540
avgAverages a numeric field (0 for an empty array){{ orders | avg: "totalAmount" }}180
maxLargest numeric value of a field (non-numeric values ignored; 0 if none){{ products | max: "price" }}129.9
minSmallest numeric value of a field{{ products | min: "price" }}19.9
whereFilters 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
sortSorts 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 for loop simply renders nothing, never an error.
  • The personalization picker's Product catalog and Custom entity options build the assign snippet for you (see Message Variables).
Deprecated: catalog

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

  • capitalize also lowercases the remainder of the string ("mAYA" becomes Maya, not MAYA).
  • default treats an empty string as missing, so {{ user.firstName | default: "there" }} falls back even when the attribute exists but is blank.
  • truncate appends the suffix after the cut length (standard Liquid counts the ellipsis inside the length).
  • where and sort are 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.