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.

Liquid Tags Articles

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.

  1. Author. You write a message with Liquid tags in the editor. (Netcore template editor)
  2. Validate. Netcore checks the syntax — matched brackets, recognised attributes, valid filters. Errors block save. (Editor, in real time)
  3. Resolve. For each user in the audience, Netcore reads their attributes and event properties from the user profile. (Send pipeline, per user)
  4. Fetch. If Content Fetch variables are used, Netcore calls your APIs and binds the response. (Send pipeline, per user)
  5. Render. Liquid evaluates: it substitutes variables, applies filters, runs conditionals and loops. Output is plain text or HTML. (Send pipeline, per user)
  6. 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 ExpressionDescriptionOutput
{{ 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.

ContextFilters SupportedOperators(==, >, and) Supported
{% assign %}YesNo
{% if %}, {% elsif %}NoYes
{{ ... }} Output tagsYesNo
{% for %} loopsNoNo

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

1. User Attributes

These are the attributes that are built-in fields on every profile.

TagDescription
{{ 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.

  1. Default Fallback
  2. Conditional Fallback
  3. Abort Message

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 LogicDescription
if / elsif / elseDisplays different content based on matching conditions.
unlessDisplays content only when a condition is false.
case / whenHandles multiple possible values for a single variable more cleanly.
and, orCombines multiple conditions within a single statement.

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 ValueOutput
75000Welcome to our VIP programme.
25000You're 1 step away from VIP.
5000Start earning rewards on your next order.

unless: the inverse of if

{% unless has_app_installed %}
  Get the app for exclusive offers.
{% endunless %}

Output renders as:

has_app_installed ValueOutput
falseGet the app for exclusive offers.
trueNo message is displayed.

case / when

Cleaner 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 ValueOutput
"hi"नमस्ते
"mr"नमस्कार
"ta"வணக்கம்
Any other valueHello

Combining conditions with 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=andOutput
tier = "Gold" and points = 1500Redeem your points before they expire.
tier = "Gold" and points = 500No message is displayed.

Output renders as:

Condition=orOutput
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

VariableDescription
forloop.indexCurrent item position starting from 1
forloop.index0Current item position starting from 0
forloop.firstChecks if it is the first item
forloop.lastChecks if it is the last item
forloop.lengthTotal number of items in the loop

Filters Reference

String Filters

FilterDescriptionExample
upcaseConverts text to uppercase"janet""JANET"
downcaseConverts text to lowercase"JANET""janet"
capitalizeCapitalises the first letter"janet doe""Janet doe"
append: xAdds text at the end"hello""hello!"
prepend: xAdds text at the beginning"9999""+919999"
stripRemoves spaces from both ends" janet ""janet"
lstripRemoves spaces from the beginning" janet""janet"
rstripRemoves spaces from the end"janet ""janet"
strip_newlinesRemoves all line breaksMulti-line → Single line
newline_to_brConverts line breaks to <br />Useful for emails
replace: a, bReplaces all matching text"+91-9999""+919999"
replace_first: a, bReplaces only the first match"a-b-c""a_b-c"
remove: xRemoves all matching text"hello world""hell wrld"
remove_first: xRemoves only the first match"a-b-c""ab-c"
slice: i, nExtracts part of a string"+919999""+91"
truncate: nShortens text to a fixed length"Hello world""Hello…"
truncatewords: nShortens 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

FilterDescriptionExample
plus: nAdds a number1015
minus: nSubtracts a number107
times: nMultiplies a number1011.8
divided_by: nDivides a number102.5
modulo: nReturns the remainder after division101
absConverts to absolute value-4242
round: nRounds to specified decimal places11.87611.88
ceilRounds up to the nearest integer4.25
floorRounds down to the nearest integer4.84
at_least: nReturns the minimum allowed value35
at_most: nReturns the maximum allowed value85

Date Filters

TokenDescriptionExample
%dDay of the month05
%BFull month nameAugust
%bShort month nameAug
%mMonth number08
%YFour-digit year2026
%H:%M24-hour time format17:30
%I:%M %p12-hour time format05:30 PM

Array filters

FilterDescription
firstReturns the first item in an array
lastReturns the last item in an array
sizeReturns the number of items in an array or characters in a string
join: ", "Combines array items into a single string
reverseReverses the order of items in an array
sortSorts items in ascending order
uniqRemoves duplicate items from an array
compactRemoves empty or nil values from an array
map: "key"Extracts a specific property from each item in an array

URL and Encoding Filters

FilterDescription
url_encodeConverts text into a URL-safe format
url_decodeConverts encoded URL text back to normal text
escapeConverts special HTML characters into safe HTML entities
escape_onceEscapes HTML characters only if they are not already escaped
strip_htmlRemoves all HTML tags from a string

Default and Debugging Filters

FilterDescription
default: xUses the fallback value if the original value is missing, empty, or false
jsonConverts a value into JSON format for debugging or inspection
inspectDisplays 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

OperatorMeaningExample
==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 %}
andBoth conditions true{% if a == 1 and b == 2 %}
orEither condition true{% if city == "BLR" or city == "MUM" %}
containsSubstring or array membership{% if tags contains "VIP" %}

Special values

ValueDescription
nilAttribute not present on the user profile. In LiquidJS, rendered as empty string (not the text "undefined").
blankEmpty string, empty array, nil, null, or undefined — covers all "no useful value" states. Prefer this for missing-data checks.
emptySpecifically an empty string or array (not nil/null)
true · falseBoolean 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.

TagPurpose
{% 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

ChannelLiquid supportFields that accept Liquid
App Push (APN)Available nowTitle, Body, Image URL, Deep Link
WhatsAppAvailable nowBody variables, Header variables, Media URL, Button URL parameters
EmailComing nextSubject, Preheader, HTML body, Sender name
SMSComing nextMessage body
Web PushComing nextTo be disclosed later
In AppComing nextTo be disclosed later
Web messageComing nextTo be disclosed later
RCSComing nextTo 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 ToolDescription
Inline Syntax ValidationThe editor validates Liquid syntax in real time and highlights issues like invalid tags, mismatched brackets, or unknown attributes before saving.
Personalised PreviewPreview the fully rendered message for specific users using their actual attributes, event data, and Content Fetch responses.
Test SendSend a test version of the rendered message to your own device or email for final verification before launch.
Skipped Users ReportView 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 PracticeDescription
Always provide a fallbackUse 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 timesStore repeated values in variables using assign instead of repeating the same logic throughout the template.
Format complex values upstreamFormat prices, balances, currencies, and locale-specific values in your API or CDP before sending them to Liquid.
Comment complex logicAdd comments to explain conditional branches and business logic for easier maintenance later.
Test with incomplete user dataValidate templates using users with missing fields, unusual characters, or outdated attributes to ensure graceful rendering.
Keep Content Fetch APIs lightweightOptimize 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:

  1. Brackets come in pairs. Every {{ needs a closing }}; every {% if %} needs {% endif %}; every {% for %} needs {% endfor %}.
  2. Use straight quotes ' ", not curly quotes. Curly quotes silently break Liquid. They sneak in when pasting from Word, Notes, or Slack.
  3. Always provide a fallback for user attributes. Real-world data is missing somewhere. See Fallbacks & defaults.
  4. Don't put Liquid inside HTML comments <!-- ... --> in email. The renderer strips comments before Liquid runs, so the tag never resolves.
  5. 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.
  6. Falsy values in LiquidJS are false, null, and undefined — not just nil and false as in Ruby Liquid. Use == blank to catch empty strings and arrays too.
  7. 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.
  8. Attribute placeholders previously written as [attribute] should now be updated to the Liquid format : {{attribute}} for all new templates.
  9. Existing templates will continue to function without any changes.
  10. 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 }}?

A. It could be because of three common causes:

  1. You pasted from a rich-text source and your {{ turned into curly quotes. Re-type the brackets directly.
  2. You're editing in the rich-text editor instead of the HTML editor. Switch to HTML.
  3. The Liquid is wrapped in an HTML comment. Remove the comment tags.

Q. Why {% 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.