Message Variables
Personalize any journey message (email, SMS, push, WhatsApp, webhook) with Liquid placeholders. A variable that has no value renders as empty text - it is never an error.
The two event namespaces
Journeys give you two event namespaces, and the difference matters:
trigger- how they entered. The entry event, fixed for the whole run.event- the latest thing they did. The most recent event that advanced the journey, such as the reply that resolved a Behavior Split. It starts equal totrigger, then re-binds each time a wait resolves.
So for "what the contact just replied," use event (or the reply aliases
below) - not trigger.
User - default vs custom attributes
Default attributes - standard fields, written as {{ user.firstName }}:
| Variable | Value |
|---|---|
user.firstName, user.lastName | Name attributes |
user.email, user.phone | Contact fields |
user.id | Joryio's own contact id |
user.externalId | Your external id |
user.whatsappName | WhatsApp profile name synced to the contact |
Custom attributes - anything else on the contact, under the custom
namespace, e.g. {{ user.custom.plan }}:
| Variable | Value |
|---|---|
user.custom.<attribute> | Any custom attribute (e.g. user.custom.plan) |
The older flat form {{ user.<attribute> }} still resolves for back-compat, but
{{ user.custom.<attribute> }} is the recommended way to reference custom
attributes - it keeps them visually distinct from default fields.
Default attribute names are reserved - you can't create a custom attribute with
the same name as a default (e.g. email, country, firstName), so the two never
collide and the picker never lists a name twice.
Inserting variables
The WhatsApp, SMS, push, and webhook editors have a + Personalize button that opens the Add Personalization picker. Pick a type, choose the attribute, add an optional default value, then Insert the generated snippet:
- Latest event - the latest event that advanced the journey. Pick a source:
Reply (WhatsApp) (the friendly
reply.*aliases) or Custom event (event.*). - Trigger (entry) - how they entered. Pick a source: WhatsApp message or Custom event.
- Default attributes -
{{ user.firstName }},{{ user.email }}, and the other first-class fields (the same list as the segment "default attribute" picker). - Custom attributes - your org's custom attributes only (defaults are listed
under their own type, never both), inserted as
{{ user.custom.<attribute> }}. - Product catalog - records from your synced store catalog via a product Selection,
inserted as an assign tag such as
{% assign products = 'featured' | products %}that you then loop over with{% for item in products %} … {% endfor %}. - Custom entity - records from a custom entity via a
saved Selection, inserted as
{% assign items = 'tv_series' | entity: 'latest' %}and looped the same way.
The optional default value appends a | default: "…" filter, so an empty attribute
falls back to your text instead of rendering blank.
Email and in-app
The visual email and in-app editors have a Personalization button that opens the same picker in copy mode: build the snippet, copy it, and paste it wherever you want in your design. (Reply / Latest event / Trigger aren't offered there - those resolve only inside a journey.)
Trigger (entry event)
| Variable | Value |
|---|---|
trigger.properties.<field> | Any property the entry event carried |
trigger.properties.content.text | Inbound text, if entry was a WhatsApp message |
Event (latest advancing event) and reply aliases
After a Behavior Split that waits for a reply, event is that reply. The
reply aliases are friendly shortcuts for inbound messages - they work the same
for both a WhatsApp reply and an SMS reply. After an SMS Inbound Message
Behavior Split, reply.text is the contact's latest inbound SMS text.
| Variable | Alias | Value |
|---|---|---|
event.properties.content.text | reply.text | The reply text |
event.properties.type | reply.type | Inbound message type (text, image, button…) |
event.properties.profile.name | reply.profile.name | WhatsApp profile name |
event.name | - | The event name |
event.properties.<field> | - | Any other captured property |
Example - a WhatsApp reply after a Behavior Split
Thanks {{ reply.profile.name | default: "there" }} - you said
"{{ reply.text }}". We'll follow up shortly.
Use trigger.* for how they entered and event.* / reply.* for what they
just did. For a scheduled-entry journey, trigger has no inbound content - the
reply after a split lives on event / reply.