Personalizing messages with liquid
UpdatedLiquid is a templating language that lets you personalize messages based on your audience’s data. Any part of your message can contain liquid, meaning that you’re not just limited to personalized message content: you can use liquid to personalize your sender information, email subjects, and so on.
New to liquid? Check out our tutorial for marketers!
Our tutorial here can help you understand the fundamentals of message personalization and how to use your data to send engaging messages that increase conversion rates.
How it works
Liquid lets you use variables and other logical statements in messages. These can be customer attributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. Attributes are analogous to traits in Data Pipelines., event data, and so on. When we send a message (commonly called a deliveryThe instance of a message sent to a person. When you set up a message, you determine an audience for your message. Each individual “send”—the version of a message sent to a single member of your audience—is a delivery.), we process the liquid syntax in your messages, generating a message personalized for the individual recipient.
For example, if you store a person’s first name as an attribute called first_name
, you can reference a person’s name as {{customer.first_name}}
. Then, whenever you send that message to a person, we’ll use that person’s first name in the message.
If your liquid statements don’t evaluate properly—like, if a person didn’t have a first_name
attribute from our example above—a message won’t render, and we’ll log a Failed state for that message. To get around these kinds of issues, you can add fallback statements and other logic in your liquid syntax that determines what to render when someone doesn’t have a variable.
We have an example of a fallback statement to make sure that your message renders even if a person doesn’t have a first_name
attribute.
Hi {{customer.first_name | default:"Buddy"}}!
The default
filter only works with our latest version of liquid.
Find more about liquid versions on our Liquid upgrade page. If you’re using our legacy liquid, you can use if/else statements for fallbacks.
{{customer.first_name}}"] a-->b{Does customer have
first_name attribute?} b--->|yes|c[Hi Alex!] b--->|yes|d[Hi Blair!] b-.->|no|e{Is there a fallback?} e-->|yes|f[Hi Buddy!] e-.->|no|g[Message failure] style c fill:#B5FFEF,stroke:#007069 style d fill:#B5FFEF,stroke:#007069 style f fill:#B5FFEF,stroke:#007069 style g fill:#FFC4CF,stroke:#69002C
Liquid personalization variables
While you can access a lot of information with Liquid, there are generally three types of variables you’ll use to personalize messages for your audience: attributes, event properties, and trigger properties.
- attributes: use the
customer
scope—{{customer.<attribute_name>}}
; these are our most common variables, and they’re available in almost every context. - event properties: use the
event
scope—{{event.<data.property>}}
from thedata
object in an event that triggers a campaign; these - trigger properties
- You can use the
trigger
scope -{{trigger.<data.property>}}
- to reference data from an incoming webhook that triggers a campaign or from thedata
object in a transactional message or API-triggered broadcast. - You can use the
trigger
scope -{{trigger.<object_type>.<attribute_name>}}
- to reference data from an object that triggers a campaign. - You can use the
trigger
scope -{{trigger.relationship.<attribute_name>}}
- to reference data from a relationship that triggers a campaign or from a relationship to an object that triggers a campaign.
- You can use the
You can reference attributes (using the customer
scope) in any message. You can only use event
properties in a campaign that uses a trigger event. You can use trigger
properties to target trigger data in API-triggered broadcasts, transactional messages, and campaigns triggered by webhooks, objects, or relationships.
When you create a message, we show a Sample data section with representative attribute and event or trigger property examples that you can use to personalize your message.
You can also access meta variables about campaigns, messages, etc.
We expose a number of meta variables—variables about a message itself—that you can use with Liquid. You probably don’t want to expose these to your audience, but it’s possible that you might use them.
Attribute variables
The {{customer.<var>}}
scope represents attributes associated with people. You can reference customer variables in any
message or action except when working with anonymous events.
If you want to include a dynamic customer attribute, you’ll want to include the tag that references that attribute. For example, if someone has an attribute on their profile called first_name
, the tag would be: {{ customer.first_name }}
.
As you can see, anything you’ve included as a customer attribute should start with customer.
before your attribute name. If you’ve got country
, for example, it’d be {{ customer.country }}
Event data variables
Let’s use a simple example: imagine one of your customers bought a pair of socks, and you want to send them a receipt for those socks. To do that, you send Customer.io a purchase
event, with data for the socks they bought, and how much those socks cost. Say the data that Customer.io received looks like this (JavaScript):
analytics.track("purchase", { price: "23.45", product: "socks" });
_cio.track("purchase", { price: "23.45", product: "socks" });
You could then use that data in your message content (be it email, push, in-app, or anything else) like this:
You just bought {{ event.product }} for {{ event.price }}!
This outputs “You just bought socks for 23.45!” You’ll see that though the event is named “purchase”, you still use the notation event
to reference it in your message.
Outside of the event
scope, which represents the data
from your event, you can also access meta information about the event.
event_name
represents the event name, as sent into Customer.ioevent_id
represents the ID as logged by Customer.io; this value is always unique.event_timestamp
is the event timestamp in Unix epoch format.
You can only use data from the event that triggered your campaign
Even if you reference other events in your campaign—like with a wait, or as a conversion condition, you can only use properties from the event that triggered your campaign in liquid. You can’t reference an outside event.
Trigger properties
API-triggered broadcasts, transactional messages, and webhook-triggered campaigns
When you send an API-triggered broadcast, a transactional message, or use liquid to access properties in an incoming webhook that triggers a campaign, you reference properties from the trigger event as {{trigger.<property>}}
.
For example, if your transactional message is designed to send shipping updates, you might include information about the item(s) that you’re shipping to your audience and when they’ll arrive, like this:
Your {{trigger.product}} has shipped! It will arrive in {{trigger.minShipTime}} to {{trigger.maxShipTime}} {{trigger.unitOfTime}}.
And you could expect that to output, “Your trampoline as shipped! It will arrive in 4 to 6 weeks.”
See API-triggered broadcasts and transactional messages for more information.
Object and relationship-triggered campaigns
You can reference properties from the trigger event when an object or relationship triggers a campaign.
For example, if you trigger a campaign when an account object is updated, like the account name, you could include the new name of the account in an email with this liquid:
Account ID {{trigger.account.id}} has changed: {{trigger.account.name}}
If if you trigger a campaign when a relationship is added to an account object, you could include this liquid:
You have been added as a/an {{trigger.relationship.role}} on this account: {{trigger.account.name}}.
See object-triggered campaigns and relationship-triggered campaigns for more information. See Use objects in liquid for more examples.
JSON notation and liquid
Liquid uses JSON dot notation to access nested variables. If you’re new to JSON and organizing data, check out our introduction to JSON. But, all this means is that you can store and access nested properties in liquid!
For example, imagine that someone refers a customer to you, and you store that referral as an attribute. You might have an attribute called referrer
that contains information like, how the referring party is related to the new customer, etc.
You could access those nested properties with {{customer.referrer.relationship}}
or customer.referrer.name
.
Liquid Fallbacks
Messages will fail if you attempt to use data that does not exist—like when a person doesn’t have an attribute or an event doesn’t have a property you reference. To prevent this kind of error, you should implement a fallback to a static value when a variable (attribute, event property, etc) doesn’t exist.
Right now, we have two versions of liquid - “legacy” liquid and the “latest” liquid. The fallback logic you’ll use depends on which version you’re on. Eventually, everyone will move to our latest liquid, so if your account uses the legacy liquid version in any message, we recommend you create a fallback that covers both legacy AND latest liquid versions. Learn about our liquid upgrade and how to see which version you’re on.
Fallback that covers both liquid versions
You can specify a condition that covers both liquid versions, so you can, for instance, use the same snippet across all emails no matter what liquid version they use.
Fallback for latest liquid
The fallback for the latest liquid does not require an if
statement, like the legacy liquid. In fact, you cannot use !=/== blank
in the latest liquid to create conditions. Instead, you should use the default
filter to set a fallback value when a person does not have the attribute specified in the liquid. For example, if a subset of people have a value for an attribute named plan_name
, you would create a fallback like this:
You are currently on our {{ customer.plan_name | default:"custom" | capitalize }} plan.
That will show, for instance, You are currently on our Trial plan.
for people who have this value for their plan_name
attribute. It will show You are currently on our Custom plan.
for people who do not have a value for their plan_name
attribute.
In our rich text editor, we also have a shortcut for you!
This isn’t available in our code or drag and drop editors, though.
Fallback for legacy liquid
The fallback for legacy liquid includes an if
statement. You can use the following shortcut to add a legacy liquid fallback in our rich text editor. This is not available in our code or drag and drop editors.
If you’re using our code or drag and drop editors, here’s how you would write the legacy liquid logic from scratch. For example, if a subset of people have a value for an attribute named plan_name
, you could create a fallback like this:
{% if customer.plan_name != blank %}
You are currently on our {{ customer.plan_name | capitalize }} plan.
{% else %}
Please choose a plan.
{% endif %}
If the person has a plan_name
value of Trial
, then the liquid will display, You are currently on our Trial plan.
If the person’s plan_name
is blank, the liquid will display, Please choose a plan.
Alternatively, if you would rather not show anything for people who do not have a value for their plan_name
attribute, you can just leave off the else
statement:
{% if customer.plan_name != blank %}
You are currently on our {{ customer.plan_name | capitalize }} plan.
{% endif %}
That will show, You are currently on our Trial plan.
for people who have this value for their plan_name
attribute or nothing at all for people who do not have a value for their plan_name
attribute.
Liquid message meta variables
If you want to personalize messages for an audience of people, you’re probably looking for Personalization variables above.
When we render messages—when you preview a message or we generate a delivery—we produce meta-information about your message, campaign, etc that you can reference with liquid. These variables generally aren’t meaningful to your audience. You would not want to expose them in a message, but you might use them programmatically to construct custom unsubscribe links, etc.
The variables available to your message may change based on the type of workflow, message, action, that you reference. While many variables are available when you preview a message, some, like deliveries
only populate at send time.
variable | availability | description |
---|---|---|
editor | always | Describes the type of editor used to create an action. One of bee (email), html
(email), json (custom push payload), wysiwyg (email, push notification, in-app, SMS,
WhatsApp). For Create or update person actions, this value is blank. |
action.id | attribute updates | Describes the ID of an action in a workflow. |
action.name | attribute updates | Describes the name of an action in a workflow. |
action.type | attribute updates | Because actions are always attribute updates, this value is _always_ attribute_update_action . |
action.subject | attribute updates | The attribute changed as a result of the action. |
campaign.id | always | The identifier for the campaign, newsletter, broadcast, etc. "Campaign" in this context represents any workflow. |
campaign.name | always | The name of a campaign. |
campaign.type | always | The type of campaign/workflow. Can be one of behavioral (segment-triggered campaign),
date , form , newsletter , transactional (event-triggered
campaign), transactional_message (a transactional message), triggered_broadcast . |
delivery_id | deliveries (at send time) | The specific instance of a message sent to a person. We populate this value at send time; if you reference this value in any kind of preview, the value will be empty (because a preview does not generate a delivery). |
layout.id | emails where editor: "wysiwyg" or editor: "html" | The saved layout used to create the message (if any). |
layout.name | emails where editor: "wysiwyg" or editor: "html" | The identifier for a campaign. |
message.name | Message actions, including newsletters and transactional messages | The name of a message action that you set in the workflow. |
message.id | Message actions, including newsletters and transactional messages | The identifier for a message, set by Customer.io. |
message.type | Message actions, including newsletters and transactional messages | The type of message action, one of email_action , push_action ,
slack_action , twilio_action ,
webhook_action . |
message.journey_id | Message actions, including newsletters and transactional messages | The identifier for the workflow path that the message action belongs to. |
message.send_to_unsubs | Message actions, including newsletters and transactional messages | If true, the message action is sent to unsubscribed people. |
Filters and tags
Filters and tags make up the foundations of Liquid, and there are a number of them that you can use. In the first-name example, if you wanted to make sure it was capitalized, add a capitalize
filter to the tag:
{{ customer.first_name | capitalize }}
If the field was full_name
and you wanted to include only the first word/name, you could use:
{{ customer.full_name | split: " " | first }}
Check out the other filters and tags (and how to write them) in our complete Liquid documentation.
More examples: what else can you do?
Use the Add Liquid option in the drag-and-drop email editor
If you use our drag-and-drop email editor to add user data, and you have code that involves logical or comparison operators (&
, >
, or <
), use the “Add Liquid” option in the text dropdown, rather than typing liquid manually.
Countdown to an event
If your customers subscribe to an event—a webinar, they buy tickets to a movie or concert, etc—you can include a countdown to an event or specific date and time that your audience is interested in! The timer produces an animated GIF that counts down to the event.
The countdown timer GIF cannot contain more than 60 frames. This limits the size of the image in your messages and ensures that your message loads properly. So, if you set the resolution
for your timer to seconds, the counter will stop 60 seconds after the image loads. The countdown image reloads when a person opens the message again, so it’ll always be relevant to the user’s current time, but it cannot count down indefinitely.
{% countdown point:64 font:roboto weight:light fg:000000 bg:f2f6f9 time:"2022-07-04 12:00:00 (GMT)" locale:en looping:true resolution:S frames:2 %}
Countdown timers take several parameters. Your timer must include the font size, the foreground (font) color, the background color, and the time you want to count down to.
Parameter | required | format | default | description |
---|---|---|---|---|
point | ✓ | integer | The font size for the timer | |
time | ✓ | ISO 8601 timestamp | The date and time you want to countdown to in the format YYYY-MM-DD hh:mm:ss (TZ) . Remember to close the time in quotes, as the value includes a space. | |
fg | ✓ | hex color | The foreground (font) hexidecimal color | |
bg | ✓ | hex color | The background hexidecimal color | |
font | string | inter, roboto | The font family for your timer | |
weight | string | normal | The font weight, takes normal CSS font-weight values. | |
locale | language code | en | The language you want to display: en (English), ru (Russian), jp (Japanese), zh (Chinese), pt (Portugese), es (Spanish) | |
looping | boolean | false | Determines whether the countdown timer should restart after it finishes | |
resolution | one of S, M, H, D | S | Determines how often the timer counts down—by the second, minute, hour, or day. | |
frames | integer | 1 | Number of seconds you want to show, based on the resolution, where seconds: 60, minutes: 2, hours: 1, days: 1 |
Math operations with attributes
AttributesA key-value pair that you associate with a person or an object—like a person’s name, the date they were created in your workspace, or a company’s billing date etc. Use attributes to target people and personalize messages. Attributes are analogous to traits in Data Pipelines. are stored as strings—even if the attribute value is a number or an integer. When you want to use a number/integer attribute in a math operation, or evaluate it against another number, you need to convert the attribute to a number/integer to ensure that your liquid statements evaluate properly. You can do this using plus
to add zero to the attribute.
{% assign my_attribute = customer.my_attribute | plus: 0 %}
{% if my_attribute > 0 %}
Your attribute is greater than 0!
{% else %}
Oh no, your attribute is 0 or negative.
{% endif %}
Links
You can include attributes within links as well, in order to send your users to a custom page.
Example:
<a href="http://www.yoursite.com/{{customer.ATTRIBUTE}}">
Displaying a timestamp as a regular date
Let’s say you have a customer attribute called expiration
(for a trial, maybe) that you store as a timestamp. If you wanted to display that expiration date in messages as a human-readable date, you can do so with liquid. There are a few different date filters you can use with this, but here we’re just going to show you month name, day, and year.
Example
If you had a customer attribute called expiration
with a timestamp value of 1596153600
, you can do the following:
{{customer.expiration| date: "%B %-d, %Y"}}
The result in your message would be July 31, 2020.
The spaces, commas, etc between the date filters are included in the output. So if you want 31/07/2020 you’d do %d/%m/%Y
.
For the current date, you can use:
{{ 'now' | date: "%B %-d, %Y" }}
Displaying how many days left in a trial
{% assign current_date = 'now' | date: '%s' %}
{% assign future_date = customer.trial_end %}
{{ future_date | minus: current_date | divided_by: 86400 }}
Explanation
- You can get the current epoch time with
'now' | date: "%s"
. %s is a formatting option. - The
customer.trial_end
date is in epoch time. - Since epoch time is in seconds, we’re dividing by 86400 (number of seconds in a day) to get the number of days. Keep in mind that this is integer division so it’ll be rounded down.
Compare two user or event attributes
While it’s not possible to compare attributes when you create segments or when you set up an event filter for your campaign, you can use liquid code inside your campaign’s content to achieve this purpose. The following example checks for equality:
{% if customer.attribute_1 == customer.attribute_2 %}
Hello awesome person!
{% endif %}
But you can also use operators, such as >
, <
, and so on:
{% if customer.lifetime_value > 100 %}
Thanks for being a loyal customer!
{% endif %}
Emails: Using a customer name in the ‘To’ field
This is done by using the following Liquid in your email’s ‘To’ field:
{{customer.name}} <{{customer.email}}>
Localizing date
If you want to display the current date formatted like:
06:54 AM Février 21, 2017
you can use this code replacing the months with the proper names for your desired lanuage:
{{'now' | date: "%H:%M %p %B %d, %Y" | replace: "January","Janvier" | replace: "February","Février" | replace: "March","Mars" | replace: "April","Avril" | replace: "May","Mai" | replace: "June","Juin" | replace: "July","Juillet" | replace: "August","Aout" | replace: "September","Septembre" | replace: "October","Octobre" | replace: "November","Novembre" | replace: "December","Décembre"}}
If you want to display the month in your customers’ language you can do this:
{{ event.invoice_date | date: "%-d" }}
{% case m %}
{% when '1' %}Janvier
{% when '2' %}Février
{% when '3' %}Mаrs
{% when '4' %}Avril
{% when '5' %}Mаi
{% when '6' %}Juin
{% when '7' %}Juillet
{% when '8' %}Août
{% when '9' %}Septembre
{% when '10' %}Octobre
{% when '11' %}Novembre
{% when '12' %}Décembre
{% endcase %}
{{ event.invoice_date | date: "%Y" }}
Show a different message based on day of week
{% assign day = 'now' | date: '%A' %}
{% if day == 'Friday' %}
Have a great weekend!
{% else %}
Cheers,
{% endif %}
Using data containing whitespace
We recommend adding data to Customer.io without spaces. But if you do (for example, if you send us an attribute called current city
), here’s how you’d refer to it:
{{ customer["current city"] }}
Escaping special characters in attribute names
In general, you shouldn’t use periods, commas, numbers, or special characters in attribute names or event properties, because it can make it hard to find and use your attributes. But if your attribute names do include these kinds of characters, you can use brackets and quotes to reference your attribute in liquid.
{{customer["attribute.name"]}}
Filtering out default data or specific data
{% if customer.first_name contains 'Visitor' %}
{% else %}{{ customer.first_name | capitalize }}
{% endif %}
Advanced
Drag-and-drop editor users: If you’re writing more advanced Liquid including logical or comparison operators (&
, >
, or <
, for example), use our Add Liquid dropdown option!
Looping through attributes
For this example, imagine each person has in their profile a list of friends, and you want to include this list in a message. Here’s an example of what the data you send to Customer.io might look like in JavaScript:
_cio.identify({
id: "1",
friends: ["Elaine", "George", "Kramer"]
});
Say the user with an id of 1
is “Jerry”. When you’re composing messages for Jerry, you can display his friend list using a Liquid for
loop:
{% for person in customer.friends %}
{{ person }}<br/>
{% endfor %}
And that’s it! That’ll display:
Elaine George Kramer
Looping through event data
Let’s go back to our event data example above, where a user bought some socks. Instead of making just one purchase (socks), let’s say they bought multiple items (socks, toothpaste, and dental floss), and you want to list them all in a particular message. If you sent us data like this (this example’s in Ruby):
customerio.track ( user_id, "purchase",
:items => [
{:name => "socks", :price => "23.45"},
{:name => "toothpaste", :price => "3"},
{:name => "dental floss", :price => "2.97"}
]
)
You can access that data to output a list by looping through it. This is how you’d do that:
{% for item in event.items %}
{{ item.name }} - {{ item.price }} <br/>
{% endfor %}
This will loop through your data and output the items that were passed in with the purchase
event.
Sorting looped data
This utilizes our latest liquid. Check your message to see which liquid version you’re using.
If you want to sort the data you’re looping through, you can use the sort
filter. Here’s an example of how you might sort someone’s purchases per type.
Array of purchases
"purchases": [
{
"title": "utensils",
"type": "kitchen",
"date": "today"
},
{
"title": "garlic press",
"type": "kitchen",
"date": "yesterday"
},
{
"title": "vacuum",
"type": "household",
"date": "yesterday"
}
]
Liquid to sort the groups and items in ascending order
{% assign sorted_groups = customer.purchases | group_by: "type" | sort: 'name' %}
{% for group in sorted_groups %}
{{ group.name | capitalize }}
{% assign sorted_items = group.items | sort: 'title' %}
{% for item in sorted_items %}
- {{ item.title }}
{% endfor %}
{% endfor %}
Liquid to sort the groups and items in descending order
{% assign sorted_groups = customer.purchases | group_by: "type" | sort: 'name' | reverse %}
{% for group in sorted_groups %}
{{ group.name | capitalize }}
{% assign sorted_items = group.items | sort: 'title' | reverse %}
{% for item in sorted_items %}
- {{ item.title }}
{% endfor %}
{% endfor %}