Liquid Tags
Learn how to use Liquid Tags in Netcore's CE to create dynamic, personalised content.
Learn how Liquid Tags work and how to use them effectively in your campaigns.
Each section covers a specific concept, including personalisation tags, filters, operators, conditional logic, fallback handling, _abort messages, and real-world use cases, to help you build dynamic, personalised messaging experiences.
Overview
Liquid Tags adds a powerful templating layer to Netcore CE, enabling marketers to send dynamic and personalised messaging by inserting user-specific data and conditional logic directly into their campaigns.
Suppose you want to send different messages to different users like Mumbai vs Delhi, men vs women, or inactive users vs recent buyers.
You would probably create separate segments, duplicate the campaign, change the copy for each segment, and send them individually.
Liquid Tags changes this entirely. You write one template. Netcore does the rest.
Instead of sending the same static copy to an entire segment, with Liquid tags , you can create a single message template that automatically adapts for each user based on their attributes, behaviour, and interactions.
Important Point to Remember
- As part of Beta release, Liquid Tags are currently for App Push Notifications (APN), WhatsApp channels in newly created campaigns and journeys. It will soon be available for all channels.
- To activate Liquid Tags, contact [email protected].
- Existing campaigns and journeys are not affected.
Working of Liquid Tags
A message containing Liquid moves through a strict, deterministic pipeline before it leaves Netcore. Understanding the pipeline helps you debug issues and design fallbacks correctly.
- Author. You write a message with Liquid tags in the editor. (Netcore template editor)
- Validate. Netcore checks the syntax — matched brackets, recognised attributes, valid filters. Errors block save. (Editor, in real time)
- Resolve. For each user in the audience, Netcore reads their attributes and event properties from the user profile. (Send pipeline, per user)
- Fetch. If Content Fetch variables are used, Netcore calls your APIs and binds the response. (Send pipeline, per user)
- Render. Liquid evaluates: it substitutes variables, applies filters, runs conditionals and loops. Output is plain text or HTML. (Send pipeline, per user)
- Send. The fully rendered message is handed to the channel for delivery. (APNs / WhatsApp / Email / RCS)
Getting Started
Every Liquid feature in this document builds on the same three-step rhythm. Walk through it once and the rest will feel familiar.
1. Insert Variable
Click on the Personalisation icon and the editor surfaces available attributes. Pick one.
Hi {{ first_name }}, welcome to Netcore!
Output: When first_name = "Janet": Hi Janet, welcome to Netcore!
2. Add Fallback
Some users won't have a first name on file. Pipe the variable through default to set a safe fallback.
Hi {{ first_name | default: "there" }}, welcome to Netcore!
Output: When first_name is missing: Hi there, welcome to Netcore!
3. Add Condition
Show different copy depending on a user's tier. One campaign, branching content.
Hi {{ first_name | default: "there" }},
{% if loyalty_tier == "Gold" %}
Your {{ points }} points unlock 20% off today.
{% elsif loyalty_tier == "Silver" %}
Members like you get 10% off, today only.
{% else %}
Free shipping on us, no minimum.
{% endif %}
Output: When Loyalty tier is Gold with 1000 points in the wallet: Hi Janet, You 1000 points unlock 20% off today.
Syntax Fundamentals
Netcore Liquid is Shopify-compatible. Tags or syntax used Liquid in Shopify, or another product, works in Netcore.
Types of Liquid Tags
- Output Tag: These are used to display values within the message. They render the content visible to the customer.
Hi {{ first_name }}
If firstname = _Aisha, the customer sees: Hi Aisha
- Logic Tag: These are used to control behaviour and decision-making inside the template. They handle conditions, loops, variable assignments, and other processing, but do not display content directly.
{% if loyalty_tier == "Gold" %}
You unlocked 20% off!
{% endif %}
Good to Remember
{{ }}: Show something{% %}: Do something
Netcore Specific Attribute Names
You can use the attribute name directly in the Liquid tag without adding any prefixes or special identifiers.
{{ first_name }}
{{ loyalty_tier }}
{{ pin_code }}
Watch Out
- Attribute names should use
CAPITAL CASE(letters, digits).- Avoid spaces or special characters. If you have a legacy field with spaces, normalise it in your data layer before sending to Netcore.
Filters
Defination: A filter transforms or formats a value before it is displayed in the final message.
Tool Tip: You can chain filters with the pipe character |.
Refer to the table below to understand the filter tag and its usage.
| Liquid Expression | Description | Output |
|---|---|---|
{{ first_name | capitalize }} | Capitalises the first letter of the value | "janet" becomes"Janet" |
{{ first_name | downcase | capitalize | default: "there" }} | - downcase converts the value to lowercase - capitalize capitalises the first letter - default: "there" uses "there" if the value is missing or empty | "aLEx" becomes "Alex" |
{{ signup_date | date: "%d %B %Y" }} | Formats the date value | "12 August 2024" |
Assigning Variables
Description: The assign tag saves a value so you can reuse it later in the template.
Tool Tip: It creates a reusable variable inside the template.
{% assign total_balance = gift_card_balance | plus: rewards_balance %}
You have {{ total_balance | currency: "INR" }} to spend.
{% if total_balance > 500 %}
Treat yourself!
{% endif %}
You have ₹550 to spend.
Treat yourself!
Filter Usability
Filters can only be used in specific Liquid contexts. One of the most common mistakes is trying to use filters directly inside if conditions, which is not supported.
| Context | Filters Supported | Operators(==, >, and) Supported |
|---|---|---|
{% assign %} | Yes | No |
{% if %}, {% elsif %} | No | Yes |
{{ ... }} Output tags | Yes | No |
{% for %} loops | No | No |
Personalisation Sources
Every Liquid tag gets its value from a data source. Netcore supports multiple personalisation sources, allowing you to dynamically render customer attributes, event data, campaign variables, and live API responses inside your messages.
There are five personlisation sources
- Standard user attributes
- Custom User Attributes
- Events Property
- Content Fetch
- Campaign Level Variables
1. User Attributes
These are the attributes that are built-in fields on every profile.
| Tag | Description |
|---|---|
{{ first_name }} | User's first name |
{{ last_name }} | User's last name |
{{ email }} | Primary email address on file |
{{ mobile }} | Primary mobile number with country code |
{{ city }} · {{ country }} | Geographic attributes from SDK or imports |
{{ customer_id }} | Your customer ID, useful as a Content Fetch parameter |
{{ created_at }} | Profile creation timestamp |
2. Custom User Attributes
Anything you push into Netcore, loyalty tier, lifetime spend, last purchase category, preferred language. Reference them the same way as standard attributes.
{{ loyalty_tier }}
{{ lifetime_spend | currency: "INR" }}
{{ last_purchased_category }}
Common use cases include:
- Loyalty tier
- Preferred language
- Last purchase category
- Lifetime spend
- Membership status
3. User Activity Payloads
For event-triggered campaigns (abandoned cart, transaction confirmation), the properties of the triggering event are exposed under payload. The namespace stays constant: it is always payload.<property>, never the event name.
You left {{ payload.cart_value | currency: "INR" }} worth of items in your cart,
including {{ payload.top_item_name }}.
Fallbacks
It is always better to assume that some customer data may be missing. Using fallbacks ensures your messages remain clean, readable, and personalised even when an attribute is unavailable.
Without fallbacks, customers may receive broken experiences like:
Hi
Fallbacks help you safely handle missing, empty, or undefined values in your campaigns.
Default Fallback
Use the default filter to display a fallback value when an attribute is missing, empty, or undefined.
Hi {{ first_name | default: "there" }},
Your city: {{ city | default: "India" }}
Lifetime points: {{ points | default: 0 }}
-- When all values are available --
Attribute Values:
first_name = "Aisha"
city = "Mumbai"
points = 1200
`Hi Aisha,
Your city: Mumbai
Lifetime points: 1200`
-- When some values are missing --
Attribute Values:
first_name = null
city = null
points = null
`Hi there,
Your city: India
Lifetime points: 0`
Conditional Fallback
Use conditional logic to display alternate content when an attribute or value is unavailable. This is useful when the fallback requires a different message instead of a simple replacement value.
{% if last_purchased_category %}
Looking for more in {{ last_purchased_category }}?
We just dropped 30 new items.
{% else %}
Discover what's new this week, hand-picked for you.
{% endif %}
Abort Message
Use the abort_message tag to skip sending a message when critical data is missing or invalid. This helps prevent broken or incomplete customer experiences.
{% if policy_number == blank %}
{% abort_message "Missing policy_number" %}
{% endif %}
Your policy {{ policy_number }} renews on
{{ renewal_date | date: "%d %B %Y" }}.
Aborts are tracked per campaign in the Skipped Users breakdown, grouped by abort reason. This is your primary debugging surface when a campaign reaches fewer users than expected.
Conditional Logic
Conditional logic allows you to dynamically display different content based on user attributes, events, or specific conditions.
| Conditional Logic | Description |
|---|---|
if / elsif / else | Displays different content based on matching conditions. |
unless | Displays content only when a condition is false. |
case / when | Handles multiple possible values for a single variable more cleanly. |
and, or | Combines multiple conditions within a single statement. |
if / elsif / else
if / elsif / else{% if lifetime_spend > 50000 %}
Welcome to our VIP programme.
{% elsif lifetime_spend > 10000 %}
You're 1 step away from VIP.
{% else %}
Start earning rewards on your next order.
{% endif %}
Output renders as:
lifetime_spend Value | Output |
|---|---|
75000 | Welcome to our VIP programme. |
25000 | You're 1 step away from VIP. |
5000 | Start earning rewards on your next order. |
unless: the inverse of if
unless: the inverse of if{% unless has_app_installed %}
Get the app for exclusive offers.
{% endunless %}
Output renders as:
has_app_installed Value | Output |
|---|---|
false | Get the app for exclusive offers. |
true | No message is displayed. |
case / when
case / whenCleaner than chained elsif when you're branching on a single variable.
{% case preferred_language %}
{% when "hi" %} नमस्ते
{% when "mr" %} नमस्कार
{% when "ta" %} வணக்கம்
{% else %} Hello
{% endcase %}
Output renders as:
preferred_language Value | Output |
|---|---|
"hi" | नमस्ते |
"mr" | नमस्कार |
"ta" | வணக்கம் |
| Any other value | Hello |
Combining conditions with and, or
and, or{% if tier == "Gold" and points > 1000 %}
Redeem your points before they expire.
{% endif %}
{% if city == "Mumbai" or city == "Pune" %}
Same-day delivery available.
{% endif %}
Output renders as:
Condition=and | Output |
|---|---|
tier = "Gold" and points = 1500 | Redeem your points before they expire. |
tier = "Gold" and points = 500 | No message is displayed. |
Output renders as:
Condition=or | Output |
|---|---|
city = "Mumbai" | Same-day delivery available. |
city = "Pune" | Same-day delivery available. |
city = "Delhi" | No message is displayed. |
Loops
Loops allow you to iterate through arrays or lists and dynamically render content for each item in the collection.
{% for product in recommended_products %}
<div class="product">
<img src="{{ product.image }}" />
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency: "INR" }}</p>
</div>
{% endfor %}
Limiting and Slicing
Use limit and offset inside loops to control how many items are displayed and which items to display from an array.
# First 3 items
{% for product in recommended_products limit: 3 %}
<li>{{ product.name }}</li>
{% endfor %}
# Items 4 to 6
{% for product in recommended_products offset: 3 limit: 3 %}
<li>{{ product.name }}</li>
{% endfor %}
Special variables inside a loop
| Variable | Description |
|---|---|
forloop.index | Current item position starting from 1 |
forloop.index0 | Current item position starting from 0 |
forloop.first | Checks if it is the first item |
forloop.last | Checks if it is the last item |
forloop.length | Total number of items in the loop |
Filters Reference
String Filters
| Filter | Description | Example |
|---|---|---|
upcase | Converts text to uppercase | "janet" → "JANET" |
downcase | Converts text to lowercase | "JANET" → "janet" |
capitalize | Capitalises the first letter | "janet doe" → "Janet doe" |
append: x | Adds text at the end | "hello" → "hello!" |
prepend: x | Adds text at the beginning | "9999" → "+919999" |
strip | Removes spaces from both ends | " janet " → "janet" |
lstrip | Removes spaces from the beginning | " janet" → "janet" |
rstrip | Removes spaces from the end | "janet " → "janet" |
strip_newlines | Removes all line breaks | Multi-line → Single line |
newline_to_br | Converts line breaks to <br /> | Useful for emails |
replace: a, b | Replaces all matching text | "+91-9999" → "+919999" |
replace_first: a, b | Replaces only the first match | "a-b-c" → "a_b-c" |
remove: x | Removes all matching text | "hello world" → "hell wrld" |
remove_first: x | Removes only the first match | "a-b-c" → "ab-c" |
slice: i, n | Extracts part of a string | "+919999" → "+91" |
truncate: n | Shortens text to a fixed length | "Hello world" → "Hello…" |
truncatewords: n | Shortens text to a fixed number of words | "Hello dear world" → "Hello dear…" |
split: "," | Splits text into an array | "a,b,c" → ["a", "b", "c"] |
Number Filters
| Filter | Description | Example |
|---|---|---|
plus: n | Adds a number | 10 → 15 |
minus: n | Subtracts a number | 10 → 7 |
times: n | Multiplies a number | 10 → 11.8 |
divided_by: n | Divides a number | 10 → 2.5 |
modulo: n | Returns the remainder after division | 10 → 1 |
abs | Converts to absolute value | -42 → 42 |
round: n | Rounds to specified decimal places | 11.876 → 11.88 |
ceil | Rounds up to the nearest integer | 4.2 → 5 |
floor | Rounds down to the nearest integer | 4.8 → 4 |
at_least: n | Returns the minimum allowed value | 3 → 5 |
at_most: n | Returns the maximum allowed value | 8 → 5 |
Date Filters
| Token | Description | Example |
|---|---|---|
%d | Day of the month | 05 |
%B | Full month name | August |
%b | Short month name | Aug |
%m | Month number | 08 |
%Y | Four-digit year | 2026 |
%H:%M | 24-hour time format | 17:30 |
%I:%M %p | 12-hour time format | 05:30 PM |
Array filters
| Filter | Description |
|---|---|
first | Returns the first item in an array |
last | Returns the last item in an array |
size | Returns the number of items in an array or characters in a string |
join: ", " | Combines array items into a single string |
reverse | Reverses the order of items in an array |
sort | Sorts items in ascending order |
uniq | Removes duplicate items from an array |
compact | Removes empty or nil values from an array |
map: "key" | Extracts a specific property from each item in an array |
URL and Encoding Filters
| Filter | Description |
|---|---|
url_encode | Converts text into a URL-safe format |
url_decode | Converts encoded URL text back to normal text |
escape | Converts special HTML characters into safe HTML entities |
escape_once | Escapes HTML characters only if they are not already escaped |
strip_html | Removes all HTML tags from a string |
Default and Debugging Filters
| Filter | Description |
|---|---|
default: x | Uses the fallback value if the original value is missing, empty, or false |
json | Converts a value into JSON format for debugging or inspection |
inspect | Displays a detailed debug representation of a value |
Important Point to Remember
- Some operations are not part of the standard Liquid filter set like sum and avg over arrays, min/max over arrays, and locale-aware currency formatting (e.g., ₹1,29,900).
- For these, either format the value in your data layer or Content Fetch response before passing it to Liquid.
- Contact [email protected] to enable a custom filter for your workspace.
Operators Reference
| Operator | Meaning | Example |
|---|---|---|
== | Equal to | {% if tier == "Gold" %} |
!= | Not equal to | {% if country != "IN" %} |
> · < | Greater than, less than | {% if points > 1000 %} |
>= · <= | Greater than or equal, less than or equal | {% if age >= 18 %} |
and | Both conditions true | {% if a == 1 and b == 2 %} |
or | Either condition true | {% if city == "BLR" or city == "MUM" %} |
contains | Substring or array membership | {% if tags contains "VIP" %} |
Special values
| Value | Description |
|---|---|
nil | Attribute not present on the user profile. In LiquidJS, rendered as empty string (not the text "undefined"). |
blank | Empty string, empty array, nil, null, or undefined — covers all "no useful value" states. Prefer this for missing-data checks. |
empty | Specifically an empty string or array (not nil/null) |
true · false | Boolean literals. In LiquidJS, false, null, and undefined are all falsy. Everything else is truthy. |
Supported Tags Reference
Supported Tags Reference lists all Liquid tags available in Netcore and explains how each tag is used to control logic, variables, loops, conditions, and message rendering inside templates.
| Tag | Purpose |
|---|---|
{% assign %} | Create or overwrite a variable |
{% capture %} | Build a multi-line string and assign it to a variable |
{% if / elsif / else / endif %} | Conditional branching |
{% unless / endunless %} | Inverse conditional |
{% case / when / endcase %} | Multi-way branching on a single variable |
{% for / endfor %} | Loop over an array |
{% break %} · {% continue %} | Exit or skip a loop iteration |
{% comment / endcomment %} | Render-time comment, stripped from output |
{% raw / endraw %} | Render contents literally, without parsing Liquid |
{% abort_message "reason" %} | Skip this user and record the reason in analytics |
Channel Support
Liquid behaves identically across channels. The syntax, filters, conditionals, and loops are the same everywhere. What differs is which fields accept Liquid and what the channel does to your output after rendering.
Channel availability matrix table
| Channel | Liquid support | Fields that accept Liquid |
|---|---|---|
| App Push (APN) | Available now | Title, Body, Image URL, Deep Link |
| Available now | Body variables, Header variables, Media URL, Button URL parameters | |
| Coming next | Subject, Preheader, HTML body, Sender name | |
| SMS | Coming next | Message body |
| Web Push | Coming next | To be disclosed later |
| In App | Coming next | To be disclosed later |
| Web message | Coming next | To be disclosed later |
| RCS | Coming next | To be disclosed later |
Validate and Preview
Liquid provides multiple ways to validate, test, and debug your templates before and after sending campaigns. Refer to the table below to understand this.
| Validation Tool | Description |
|---|---|
| Inline Syntax Validation | The editor validates Liquid syntax in real time and highlights issues like invalid tags, mismatched brackets, or unknown attributes before saving. |
| Personalised Preview | Preview the fully rendered message for specific users using their actual attributes, event data, and Content Fetch responses. |
| Test Send | Send a test version of the rendered message to your own device or email for final verification before launch. |
| Skipped Users Report | View users skipped during send execution along with abort reasons to identify missing data or Liquid issues. |
Best practices
Follow these best practices to build reliable, scalable, and maintainable Liquid templates.
| Best Practice | Description |
|---|---|
| Always provide a fallback | Use the default filter for user attributes to avoid broken messages when data is missing. Reserve abort_message only for critical missing data. |
| Assign once, reuse multiple times | Store repeated values in variables using assign instead of repeating the same logic throughout the template. |
| Format complex values upstream | Format prices, balances, currencies, and locale-specific values in your API or CDP before sending them to Liquid. |
| Comment complex logic | Add comments to explain conditional branches and business logic for easier maintenance later. |
| Test with incomplete user data | Validate templates using users with missing fields, unusual characters, or outdated attributes to ensure graceful rendering. |
| Keep Content Fetch APIs lightweight | Optimize API performance and always configure fallback values to avoid delays or rendering failures during sends. |
Troubleshoot and FAQs
Q. Is there any specific rules that prevent 90% of bugs?
A. You can follow the below rules to avoid 90% of bugs:
- Brackets come in pairs. Every
{{needs a closing}}; every{% if %}needs{% endif %}; every{% for %}needs{% endfor %}. - Use straight quotes
'", not curly quotes. Curly quotes silently break Liquid. They sneak in when pasting from Word, Notes, or Slack. - Always provide a fallback for user attributes. Real-world data is missing somewhere. See Fallbacks & defaults.
- Don't put Liquid inside HTML comments
<!-- ... -->in email. The renderer strips comments before Liquid runs, so the tag never resolves. - Filters can't be used everywhere. Filters work in
{% assign %}and output tags. They do not work inside{% if %}conditions. Assign to a variable first. - Falsy values in LiquidJS are
false,null, andundefined— not justnilandfalseas in Ruby Liquid. Use== blankto catch empty strings and arrays too. - When inserting an attribute into Liquid code, remove the double curly braces ({{ }}) that get added by default, the liquid expression will not evaluate correctly otherwise.
- Attribute placeholders previously written as [attribute] should now be updated to the Liquid format : {{attribute}} for all new templates.
- Existing templates will continue to function without any changes.
- User activity payloads must follow the
{{payload.payload_name}}structure when entered manually. The payload. prefix is fixed regardless of the event selected.
Only payload_name changes based on the user activity payload you wish to configure.
Q. Why the variable is rendering as {{ first_name }}?
{{ first_name }}?A. It could be because of three common causes:
- You pasted from a rich-text source and your
{{turned into curly quotes. Re-type the brackets directly. - You're editing in the rich-text editor instead of the HTML editor. Switch to HTML.
- The Liquid is wrapped in an HTML comment. Remove the comment tags.
Q. Why {% if %} is throwing a syntax error?
{% if %} is throwing a syntax error?A. Filters are not allowed inside if conditions. Move the filter to an {% assign %} first.
# Wrong — filter inside if condition
{% if tags | size > 3 %} ... {% endif %}
# Correct — assign first
{% assign tag_count = tags | size %}
{% if tag_count > 3 %} ... {% endif %}
Q. My Content Fetch variable is blank in production but works in preview?
A. Most likely your API is failing under load — timeout, rate-limit, or auth header mismatch. Check the campaign's Skipped Users report and Settings → Content Fetch → Logs. Confirm your fallback is sane; if the variable is empty, that's what will render.
Q. Can I call multiple APIs in one message?
A. Yes. Each Content Fetch variable is its own configuration. Reference any number of them in a single template. They are called in parallel.
Q. Do I need to escape HTML inside email Liquid?
A. If a user attribute could contain <, >, or &, pipe it through | escape. For names and most marketing data this rarely matters, but for free-text fields (review snippets, support tickets), always escape.
Q. How does Liquid handle timezones?
A. Date filters render in the workspace's default timezone unless overridden. To render in the user's timezone, set the user's timezone attribute on the profile, then pass it as the second argument:
{{ payload.timestamp | date: "%I:%M %p", timezone }}
Q. What's the largest array I can loop through?
A. For send-pipeline performance, keep loop iterables under 50 items. Use limit: to enforce this regardless of how large the source array is.
Q. Why does my campaign skip more users than expected?
A. Check the Skipped Users breakdown in the send report. Each abort_message call is logged with its reason, so you can see exactly which guard is firing and on which sub-segment. The most common cause is a critical attribute missing on a slice of the audience that wasn't represented in Personalised Preview.
Q. Where do I report a Liquid bug?
A. Send the campaign ID, the offending snippet, and the rendered output to your CSM, or open a ticket in Help → Support. Liquid issues are typically resolved the same day.
Updated about 3 hours ago
