> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using Liquid syntax

> Personalize email, push, SMS, in-app, and Live Activity content with Liquid tags, filters, conditionals, and loops — powered by OneSignal data sources like tags, properties, custom data, and Data Feeds.

## 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 Liquid theme={null}
Hi {{ first_name | default: "there" }}!
You have {{ message.custom_data.cart.size }} items in your cart.
```

OneSignal supports two syntax structures:

| Syntax                      | Purpose                                 | Example                              |
| --------------------------- | --------------------------------------- | ------------------------------------ |
| **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](https://shopify.github.io/liquid/basics/introduction/). 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](./message-personalization#supported-fields-by-message-type) for the full per-channel breakdown.

***

## Prerequisites

* A [OneSignal account](https://dashboard.onesignal.com).
* The data you want to reference is already available to OneSignal — for example, [Tags set on the user](./add-user-data-tags), `external_id`, [Custom Event](./personalization-custom-event) properties on a Journey, or [`custom_data` in the Create Message API](./personalization-api-custom-data).
* A message or [template](./templates) where you can edit the field that will use Liquid.

***

## Conditionals

### `if`, `elsif`, `else`

Render different content based on a value.

```liquid Liquid theme={null}
{%- 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 Liquid theme={null}
{% unless level == "1" %}
  Great job getting past the first level!
{% endunless %}
```

<Note>
  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.
</Note>

### `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 Liquid theme={null}
{% 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`.

| Operator             | Meaning                                  |
| -------------------- | ---------------------------------------- |
| `==`, `!=`           | Equal, not equal                         |
| `>`, `<`, `>=`, `<=` | Numeric comparison                       |
| `and`, `or`          | Logical AND / OR                         |
| `contains`           | Substring (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 Liquid theme={null}
{% 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 Liquid theme={null}
{% 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 Liquid theme={null}
{{ 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 Liquid theme={null}
Hello {{ first_name | default: "there" }}.
```

### `date`

Format a timestamp using [strftime](https://strftime.net/) directives. The input can be a Unix timestamp, a parseable date string, or the special words `"now"` / `"today"`.

<Note>
  `"now"` is evaluated **per recipient at send time**. In test sends, it reflects when the test was sent.
</Note>

**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](./time-operators) — for example, a tag like `bill_due: 1687968776`.

```liquid Liquid theme={null}
{{ bill_due | date: "%a, %b %d, %y" }}
{{ bill_due | date: "%Y" }}
```

```text Result theme={null}
Wed, Jun 28, 23
2023
```

**From a date string:**

```liquid Liquid theme={null}
{{ "March 14, 2016" | date: "%b %d, %y" }}
```

```text Result theme={null}
Mar 14, 16
```

**Current time at send:**

```liquid Liquid theme={null}
This message was sent at {{ "now" | date: "%Y-%m-%d %H:%M" }}.
```

```text Result theme={null}
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 Liquid theme={null}
{{ 1.2 | round }}
{{ 2.7 | round }}
{{ 183.357 | round: 2 }}
```

```text Result theme={null}
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.

<Note>
  `pluralize` is a OneSignal addition — it is not part of the standard Shopify Liquid filter set. Use it inside OneSignal templates only.
</Note>

```liquid Liquid theme={null}
{{ 1 | pluralize: "singular", "plural" }}
{{ 2 | pluralize: "singular", "plural" }}
1 {{ 1 | pluralize: "person", "people" }}
2 {{ 2 | pluralize: "person", "people" }}
2 {{ "2" | pluralize: "person", "people" }}
```

```text Result theme={null}
singular
plural
1 person
2 people
2 people
```

### Math filters

Useful for casting strings to numbers and for arithmetic inside templates.

| Filter       | Example                     | Result               |
| ------------ | --------------------------- | -------------------- |
| `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.

| Filter  | Description                                              | Example                                        |
| ------- | -------------------------------------------------------- | ---------------------------------------------- |
| `size`  | Number of items in an array (or characters in a string). | `{{ cart.size }}`                              |
| `first` | First item. Equivalent to `[0]`.                         | `{{ cart.first.product_name }}`                |
| `last`  | Last item.                                               | `{{ cart.last.product_name }}`                 |
| `where` | Filter to items whose property matches a value.          | See [`where` example](#where-example) below.   |
| `join`  | Concatenate array items with a separator.                | `{{ message.custom_data.skus \| join: ", " }}` |
| `split` | Split 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 Liquid theme={null}
{% assign products = message.custom_data.products %}
{% assign kitchen_products = products | where: "type", "kitchen" %}

Kitchen products:
{% for product in kitchen_products %}
- {{ product.name }}
{% endfor %}
```

```json Custom data theme={null}
{
  "products": [
    { "name": "Vacuum",        "type": "electronics" },
    { "name": "Spatula",       "type": "kitchen" },
    { "name": "Television",    "type": "electronics" },
    { "name": "Garlic press",  "type": "kitchen" }
  ]
}
```

```text Result theme={null}
Kitchen products:
- Spatula
- Garlic press
```

### String filters

Apply string filters to adjust how values render.

| Filter          | Description                                         | Example                                                        | Output              |
| --------------- | --------------------------------------------------- | -------------------------------------------------------------- | ------------------- |
| `replace`       | Replace every occurrence of a substring.            | `{{ "hello world" \| replace: "world", "there" }}`             | `hello there`       |
| `replace_first` | Replace only the first occurrence.                  | `{{ "hello world world" \| replace_first: "world", "there" }}` | `hello there world` |
| `capitalize`    | Capitalize the first character; lowercase the rest. | `{{ "my GREAT title" \| capitalize }}`                         | `My great title`    |
| `upcase`        | Convert to uppercase.                               | `{{ "hello" \| upcase }}`                                      | `HELLO`             |
| `downcase`      | Convert to lowercase.                               | `{{ "HELLO" \| downcase }}`                                    | `hello`             |
| `strip`         | Remove leading and trailing whitespace.             | `{{ " hello " \| strip }}`                                     | `hello`             |
| `lstrip`        | Remove leading whitespace.                          | `{{ " hello" \| lstrip }}`                                     | `hello`             |
| `rstrip`        | Remove trailing whitespace.                         | `{{ "hello " \| rstrip }}`                                     | `hello`             |
| `strip_html`    | Strip HTML tags.                                    | `{{ "<p>hello</p>" \| strip_html }}`                           | `hello`             |
| `truncate`      | Shorten a string to N characters; appends `…`.      | `{{ "This is a long sentence" \| truncate: 10 }}`              | `This is a…`        |
| `truncatewords` | Shorten a string to N words; appends `…`.           | `{{ "This is a long sentence" \| truncatewords: 2 }}`          | `This is…`          |
| `prepend`       | Add a string to the beginning.                      | `{{ "world" \| prepend: "hello " }}`                           | `hello world`       |
| `append`        | Add a string to the end.                            | `{{ "hello" \| append: " world" }}`                            | `hello world`       |
| `url_encode`    | Percent-encode a string for use in a URL.           | `{{ "a b" \| url_encode }}`                                    | `a%20b`             |
| `escape`        | HTML-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](https://shopify.github.io/liquid/tags/iteration/).

```liquid Liquid theme={null}
{% for product in message.custom_data.products %}
- {{ product.name }}
{% else %}
Sorry, we're sold out of all products.
{% endfor %}
```

```json Custom data theme={null}
{
  "app_id": "YOUR_APP_ID",
  "template_id": "YOUR_TEMPLATE_ID",
  "custom_data": {
    "products": [
      { "name": "Sweater" },
      { "name": "Hat" },
      { "name": "Shirt" }
    ]
  }
}
```

```text Result theme={null}
- Sweater
- Hat
- Shirt
```

The `{% else %}` branch renders when the array is empty or undefined:

```text Result (products is empty) theme={null}
Sorry, we're sold out of all products.
```

<Warning>
  `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.
</Warning>

#### `forloop` variables

Inside a `for` block, Liquid exposes a `forloop` object with metadata about the current iteration.

| Variable         | Description                       |
| ---------------- | --------------------------------- |
| `forloop.index`  | Current iteration, starting at 1. |
| `forloop.index0` | Current iteration, starting at 0. |
| `forloop.first`  | `true` on the first iteration.    |
| `forloop.last`   | `true` on the last iteration.     |
| `forloop.length` | Total number of iterations.       |

Useful for separators:

```liquid Liquid theme={null}
{% 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 Liquid theme={null}
{% 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 %}
```

```text Result theme={null}
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 Liquid theme={null}
Hello {{ name | default: "there" }}!
```

```text Result (name = "Jon") theme={null}
Hello Jon!
```

```text Result (name is empty) theme={null}
Hello there!
```

Use `if`/`else` when the surrounding text, punctuation, or sentence structure also needs to change.

```liquid Liquid theme={null}
{% if name %}{{ name }}, shop your favorites!{% else %}Shop your favorites!{% endif %}
```

<Warning>
  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`.
</Warning>

### How do I control whitespace and newlines?

Use hyphens inside the tag delimiters to trim surrounding whitespace: `{{- ... -}}` and `{%- ... -%}`. See [Whitespace control](https://shopify.github.io/liquid/basics/whitespace/) 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](https://shopify.dev/docs/api/liquid/tags/raw).

```json Push payload theme={null}
{
  "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.

***

## Related pages

<Columns cols={2}>
  <Card title="Personalize with properties" icon="user" href="./personalization-properties-and-tags">
    Use Tags, External ID, and other stored properties to personalize messages with Liquid.
  </Card>

  <Card title="Personalize with Custom Events" icon="bolt" href="./personalization-custom-event">
    Reference Custom Event properties in Journey messages with `journey.first_event` and related objects.
  </Card>

  <Card title="Personalize with API custom_data" icon="code" href="./personalization-api-custom-data">
    Send dynamic, message-specific data through the Create Message API and render it with Liquid.
  </Card>

  <Card title="Data Feeds" icon="database" href="./data-feeds">
    Pull real-time data from your APIs into messages at send time.
  </Card>

  <Card title="Dynamic Content with CSV" icon="file-csv" href="./dynamic-content">
    Personalize push, email, and SMS at scale using CSV uploads and the `dynamic_content` Liquid object.
  </Card>
</Columns>
