Skip to main content

Overview

Liquid is a templating language OneSignal uses to insert dynamic values into messages at send time. You write Liquid expressions in supported fields; OneSignal replaces them with the matching value for each recipient just before delivery.
Liquid
Hi {{ first_name | default: "there" }}!
You have {{ message.custom_data.cart.size }} items in your cart.
OneSignal supports two syntax structures:
SyntaxPurposeExample
Output tags {{ ... }}Print a value (with optional filters).{{ user.tags.first_name }}
Logic tags {% ... %}Run a conditional, loop, or assignment.{% if level > 5 %} ... {% endif %}
For the language itself, see the official Liquid documentation. This page focuses on the parts that work in OneSignal and the OneSignal-specific data objects you can reference. Liquid support varies by channel and by field. Most notably, in-app messages support only Tag substitution (no journey.*, message.*, subscription.*, or user.* objects), and the push data field does not support Liquid at all. See Supported fields by message type for the full per-channel breakdown.

Prerequisites


Conditionals

if, elsif, else

Render different content based on a value.
Liquid
{%- assign userLang = subscription.language -%}
{% if userLang == "es" -%}
Hola {{ first_name }}!
{%- elsif userLang == "fr" -%}
Bonjour {{ first_name }}!
{%- else -%}
Hello {{ first_name }}!
{%- endif %}

unless

Inverse of if — runs the block when the condition is false.
Liquid
{% unless level == "1" %}
  Great job getting past the first level!
{% endunless %}
Tag values are stored as strings. If you compare a numeric tag, either compare against a string (level == "1") or cast first with {% assign n = level | plus: 0 %} and then compare numerically.

case / when

Map a single variable to specific values with a fallback else. Useful when interpolating the variable directly would produce invalid output (for example, an unsupported language code in a URL).
Liquid
{% case subscription.language %}
  {% when "es" %}https://example.com/es/page
  {% when "fr" %}https://example.com/fr/page
  {% when "de" %}https://example.com/de/page
  {% else %}https://example.com/en/page
{% endcase %}

Operators

You can use these operators inside if, elsif, and unless.
OperatorMeaning
==, !=Equal, not equal
>, <, >=, <=Numeric comparison
and, orLogical AND / OR
containsSubstring (string) or membership (array)
Liquid evaluates conditions right-to-left — not by operator precedence — and does not support parentheses for grouping. Refactor with nested if blocks if you need explicit grouping.
Liquid
{% if false and true or true %}
  Does not render. Right-to-left: (true or true) → true, then (false and true) → false.
{% endif %}
If you tried to read this as (false and true) or true, you would expect it to render — but Liquid groups the rightmost two operands first, so the final check is false and ....

Variables

assign

Create a reusable variable inside a template. Useful for shortening references and for casting tag values to numbers.
Liquid
{% assign cart = message.custom_data.cart %}
{% assign item_count = cart.size | plus: 0 %}

You have {{ item_count }} items in your cart.
First item: {{ cart.first.product_name }}

Bracket notation for special characters

Use bracket notation when a key contains spaces, hyphens, or other non-alphanumeric characters.
Liquid
{{ user.tags["order status"] }}
{{ journey.first_event.properties["order-id"] }}
{{ message.custom_data["full name"] }}

Filters

Apply filters with {{ variable | filter }}. You can chain filters: {{ name | downcase | capitalize }}.

default

Render a fallback value when the input is empty, null, or undefined.
Liquid
Hello {{ first_name | default: "there" }}.

date

Format a timestamp using strftime directives. The input can be a Unix timestamp, a parseable date string, or the special words "now" / "today".
"now" is evaluated per recipient at send time. In test sends, it reflects when the test was sent.
From a Unix timestamp tag: Store dates as Unix timestamps (seconds) on user tags. The same tag then works for both Liquid date formatting and Time Operator segmentation — for example, a tag like bill_due: 1687968776.
Liquid
{{ bill_due | date: "%a, %b %d, %y" }}
{{ bill_due | date: "%Y" }}
Result
Wed, Jun 28, 23
2023
From a date string:
Liquid
{{ "March 14, 2016" | date: "%b %d, %y" }}
Result
Mar 14, 16
Current time at send:
Liquid
This message was sent at {{ "now" | date: "%Y-%m-%d %H:%M" }}.
Result
This message was sent at 2022-11-02 14:39.

round

Round a number to the nearest integer, or to a specified number of decimal places.
Liquid
{{ 1.2 | round }}
{{ 2.7 | round }}
{{ 183.357 | round: 2 }}
Result
1
3
183.36

pluralize

Return the singular or plural form of a string based on a count. The count must be a whole number; strings that look like whole numbers are accepted.
pluralize is a OneSignal addition — it is not part of the standard Shopify Liquid filter set. Use it inside OneSignal templates only.
Liquid
{{ 1 | pluralize: "singular", "plural" }}
{{ 2 | pluralize: "singular", "plural" }}
1 {{ 1 | pluralize: "person", "people" }}
2 {{ 2 | pluralize: "person", "people" }}
2 {{ "2" | pluralize: "person", "people" }}
Result
singular
plural
1 person
2 people
2 people

Math filters

Useful for casting strings to numbers and for arithmetic inside templates.
FilterExampleResult
plus{{ "2" | plus: 0 }}2 (cast to number)
minus{{ 10 | minus: 3 }}7
times{{ 4 | times: 5 }}20
divided_by{{ 20 | divided_by: 4 }}5
modulo{{ 10 | modulo: 3 }}1

Array filters

Used with arrays from custom_data, Custom Event properties, or Tags.
FilterDescriptionExample
sizeNumber of items in an array (or characters in a string).{{ cart.size }}
firstFirst item. Equivalent to [0].{{ cart.first.product_name }}
lastLast item.{{ cart.last.product_name }}
whereFilter to items whose property matches a value.See where example below.
joinConcatenate array items with a separator.{{ message.custom_data.skus | join: ", " }}
splitSplit a string into an array.{{ "a,b,c" | split: "," }}

where example

Given a list of products, build a sub-list containing only the kitchen ones:
Liquid
{% assign products = message.custom_data.products %}
{% assign kitchen_products = products | where: "type", "kitchen" %}

Kitchen products:
{% for product in kitchen_products %}
- {{ product.name }}
{% endfor %}
Custom data
{
  "products": [
    { "name": "Vacuum",        "type": "electronics" },
    { "name": "Spatula",       "type": "kitchen" },
    { "name": "Television",    "type": "electronics" },
    { "name": "Garlic press",  "type": "kitchen" }
  ]
}
Result
Kitchen products:
- Spatula
- Garlic press

String filters

Apply string filters to adjust how values render.
FilterDescriptionExampleOutput
replaceReplace every occurrence of a substring.{{ "hello world" | replace: "world", "there" }}hello there
replace_firstReplace only the first occurrence.{{ "hello world world" | replace_first: "world", "there" }}hello there world
capitalizeCapitalize the first character; lowercase the rest.{{ "my GREAT title" | capitalize }}My great title
upcaseConvert to uppercase.{{ "hello" | upcase }}HELLO
downcaseConvert to lowercase.{{ "HELLO" | downcase }}hello
stripRemove leading and trailing whitespace.{{ " hello " | strip }}hello
lstripRemove leading whitespace.{{ " hello" | lstrip }}hello
rstripRemove trailing whitespace.{{ "hello " | rstrip }}hello
strip_htmlStrip HTML tags.{{ "<p>hello</p>" | strip_html }}hello
truncateShorten a string to N characters; appends .{{ "This is a long sentence" | truncate: 10 }}This is a…
truncatewordsShorten a string to N words; appends .{{ "This is a long sentence" | truncatewords: 2 }}This is…
prependAdd a string to the beginning.{{ "world" | prepend: "hello " }}hello world
appendAdd a string to the end.{{ "hello" | append: " world" }}hello world
url_encodePercent-encode a string for use in a URL.{{ "a b" | url_encode }}a%20b
escapeHTML-escape a string.{{ "<a>" | escape }}&lt;a&gt;

Iteration

for loops

Iterate over an array of items. For the full attribute list, see the Liquid for loop documentation.
Liquid
{% for product in message.custom_data.products %}
- {{ product.name }}
{% else %}
Sorry, we're sold out of all products.
{% endfor %}
Custom data
{
  "app_id": "YOUR_APP_ID",
  "template_id": "YOUR_TEMPLATE_ID",
  "custom_data": {
    "products": [
      { "name": "Sweater" },
      { "name": "Hat" },
      { "name": "Shirt" }
    ]
  }
}
Result
- Sweater
- Hat
- Shirt
The {% else %} branch renders when the array is empty or undefined:
Result (products is empty)
Sorry, we're sold out of all products.
for loops can degrade delivery performance in rare cases. Be mindful of loop usage on high-volume sends. Push channel fields contents, headings, subtitle, apns_alert, and url do not support for loops.

forloop variables

Inside a for block, Liquid exposes a forloop object with metadata about the current iteration.
VariableDescription
forloop.indexCurrent iteration, starting at 1.
forloop.index0Current iteration, starting at 0.
forloop.firsttrue on the first iteration.
forloop.lasttrue on the last iteration.
forloop.lengthTotal number of iterations.
Useful for separators:
Liquid
{% for item in cart %}{{ item.name }}{% unless forloop.last %}, {% endunless %}{% endfor %}

limit and offset

Restrict how many items a loop iterates over and where it starts.
Liquid
{% assign great_numbers = "1,2,3,4,5,6" | split: "," %}

First three:
{% for number in great_numbers limit: 3 %}{{ number }} {% endfor %}

Skip three, then take three:
{% for number in great_numbers limit: 3 offset: 3 %}{{ number }} {% endfor %}
Result
First three:
1 2 3

Skip three, then take three:
4 5 6

FAQ

When should I use default vs. if/else?

Use the default filter when only the variable value needs a fallback and the surrounding text stays the same.
Liquid
Hello {{ name | default: "there" }}!
Result (name = "Jon")
Hello Jon!
Result (name is empty)
Hello there!
Use if/else when the surrounding text, punctuation, or sentence structure also needs to change.
Liquid
{% if name %}{{ name }}, shop your favorites!{% else %}Shop your favorites!{% endif %}
Do not use default when the sentence structure changes. For example, {{ name | default: "Shop your favorites" }}, shop your favorites would render as “Shop your favorites, shop your favorites” when the name is empty. If the fallback changes more than the variable, use if/else.

How do I control whitespace and newlines?

Use hyphens inside the tag delimiters to trim surrounding whitespace: {{- ... -}} and {%- ... -%}. See Whitespace control for the full rules.

How do I include literal {{ or {% in the output?

Wrap the section in {% raw %} and {% endraw %} so Liquid does not parse it. This is the right tool for user-generated content that may contain Liquid-looking characters. See Liquid raw syntax.
Push payload
{
  "contents": {
    "en": "{% raw %}Your user content with literal {{ braces }}{% endraw %}"
  }
}

What happens if a Liquid expression references a missing value?

The expression renders as an empty string and the message still sends. Wrap optional values in | default: "fallback" so the surrounding sentence still reads correctly.

Personalize with properties

Use Tags, External ID, and other stored properties to personalize messages with Liquid.

Personalize with Custom Events

Reference Custom Event properties in Journey messages with journey.first_event and related objects.

Personalize with API custom_data

Send dynamic, message-specific data through the Create Message API and render it with Liquid.

Data Feeds

Pull real-time data from your APIs into messages at send time.

Dynamic Content with CSV

Personalize push, email, and SMS at scale using CSV uploads and the dynamic_content Liquid object.