· 11 min read
Liquid for lifecycle marketers — the complete Braze reference
Every personalised field in every lifecycle message runs through Liquid. Get it right and your personalisation quietly improves every send. Get it wrong and you send 'Hi {{${first_name}}}' to 50,000 people. This is the reference that covers both — the syntax you need and the production patterns that prevent the bugs marketers actually hit.
Justin Williames
Founder, Orbit · 10+ years in lifecycle marketing
Why Liquid is unavoidable in Braze
Every Braze campaign that references a user attribute — a first name, a subscription end date, a loyalty points balance, a conditionally-shown paragraph — runs through the Liquid templating engine at send time. Braze Liquid is a superset of the open-source Shopify Liquid library with Braze-specific extensions like {% connected_content %} and {% abort_message %}.Source · BrazeBraze Liquid documentationOfficial Braze Liquid reference including all filters, tags, and Braze-specific extensions.www.braze.com/docs/user_guide/personalization_and_dynamic_content/liquid
When a Liquid expression fails, Braze either renders the fallback (if you specified one with | default) or ships the raw {{ ${attribute} }} text directly to the user. Missing defaults are the single most common production bug in lifecycle programs — raw template strings end up in the inbox more often than teams realise. For the judgement around whether to personalise a given surface, not just how, the personalisation guide covers where the trust line sits.
Dates — the format strings you actually need
Braze Liquid date formatting follows strftime syntax. The common formats:
%B %d, %Y→ "April 20, 2026" (full date, US-style)%d %B %Y→ "20 April 2026" (day-first, UK/AU)%Y-%m-%d→ "2026-04-20" (ISO, always unambiguous)%b %d, %Y→ "Apr 20, 2026" (abbreviated, for subject lines)%A→ "Monday" (day name only)%l:%M %p→ "2:30 PM" (12-hour clock)
Full example with fallback: {{ ${renewal_date} | date: "%B %d, %Y" | default: "your renewal date" }}. Note the order — the date filter transforms the attribute, then default catches the case where the attribute was empty or the date parse failed.
Two common gotchas. Dates stored as epoch integers need different handling than ISO strings — check your attribute's actual format before assuming. And time zones default to UTC unless you explicitly convert: pass a second argument to specify the zone, e.g. | date: "%B %d, %Y", "Australia/Sydney".
Text transforms and the safe-name pattern
The most-used text filters: | capitalize (first letter uppercase), | upcase (all uppercase), | downcase, | truncate: 80 (hard character limit with ellipsis), | truncatewords: 5 (word-aware truncation), | strip (trims whitespace).
Filters chain left-to-right. The safe-name greeting pattern — worth memorising because it handles the three most common data problems at once:
{{ ${first_name} | strip | downcase | capitalize | default: "there" }}
This normalises whitespace, handles mixed-case input (users who type "JUSTIN" in a form), title-cases the first letter, and falls back to "there" if the whole thing evaluates to blank. Use it everywhere you greet by name.
One Liquid limitation: there's no native true title-case filter. The | downcase | capitalizechain only capitalises the first letter of the entire string. For multi-word title case, you'll need to split, capitalise each, and join — or accept that single first-name greetings are the primary use case anyway.
Math — in-message computation
Math filters: | plus: 10, | minus: 3, | times: 2, | divided_by: 4, | modulo: 3, | round: 2, | ceil, | floor, | abs.
Use cases. Points balance: "{{ ${points_balance} | minus: 500 }} points to the next tier". Trial countdown: compute days remaining until subscription_end_date. Progress bars: divide completed steps by total, multiply by 100 for a percentage.
One common trap: | divided_by returns an integer by default. "{{ 7 | divided_by: 2 }}" produces "3", not "3.5". Force floating-point by dividing by a decimal literal: | divided_by: 2.0, or chain with | round: 1 to control precision.
Control flow — and the abort pattern that saves campaigns
{% if %} / {% elsif %} / {% else %} / {% endif %} let you branch on attribute values. Common patterns: plan-based content switches (pro vs free vs enterprise copy), lifecycle-stage-based CTAs, country-specific messaging.
The single most valuable control-flow pattern is {% abort_message %}. It cancels the send of the current message for the current user and logs the reason to Braze's message-level analytics. Use it as a hard guard:
{% if ${first_name} == blank %}
{% abort_message("No first name") %}
{% endif %}
The user simply doesn't receive the broken email, and the abort reason appears in Braze's analytics for debugging. Better than sending "Hi ," to 50,000 people.
{% assign %} creates a reusable variable. Useful when a value is referenced multiple times in a template — compute once, reference many. Also useful for building complex strings in stages instead of nesting filters six deep.
Connected Content — external APIs at send time
{% connected_content %} fetches an external API response at send time and makes its JSON available in the template.Source · BrazeConnected ContentBraze's feature for pulling external API data into personalisation at send time.www.braze.com/docs/user_guide/personalization_and_dynamic_content/connected_content/about_connected_contentUse for real-time inventory, personalised recommendations, content-management-system integration, or anything too volatile to store as a Braze attribute.
Example: {% connected_content https://api.example.com/recs?user={{ ${external_id} }} :save recs :cache_max_age 3600 %} then reference {{ recs.product_name }}.
Production patterns. Always cache (the :cache_max_age argument in seconds) — otherwise you'll hammer your API every send. Always include :retry for transient failures. Always include a fallback in the template in case the API returns an error or empty response. A broken Connected Content call that sends the user an empty block is worse than not personalising at all.
The production patterns that prevent silent bugs
Four habits that make the difference between a working lifecycle program and one that occasionally ships "Hi {{ ${first_name} }}," to the inbox:
1. Every personalised field gets a default.Every single one, without exception. "Hi there" is always better than "Hi ". Make the default explicit even when you think the field is always populated — data always surprises you eventually.
2. Abort before personalising anything critical. If the send is useless without a specific attribute (e.g. a shipment tracking email without the tracking number), abort rather than send the broken version.
3. Test with a profile that has missing data. Braze lets you send campaign tests to specific users. Always include at least one test user with sparse data to exercise the fallback paths. Most personalisation bugs show up here.
4. Pre-launch lint. Before a campaign ships, scan the Liquid for unbracketed attributes, missing defaults, and syntax errors. The Orbit Email Render QA skill handles this automatically on every template Orbit generates.
Frequently asked questions
- How do I format a date in Braze Liquid?
- Use the | date filter with a strftime-style format string: {{ ${date} | date: '%B %d, %Y' }} renders as 'April 20, 2026'. Pair with | default: 'some fallback' to avoid blank output if the attribute is empty.
- Does Braze Liquid have a title-case filter?
- No. Braze Liquid inherits Shopify Liquid's filters, which don't include true title case. The closest you can get is | downcase | capitalize which only title-cases the first word. For multi-word title case you'd need to split the string, capitalise each word, and join.
- How do I provide a fallback for missing personalisation?
- Use the | default filter at the end of the chain: {{ ${first_name} | default: 'there' }}. Every personalised field in production should have a default. Skipping it is the most common cause of user-visible personalisation bugs.
- What does {% abort_message %} do?
- It cancels the send of the current message for the current user and logs the reason to Braze message-level analytics. Use as a hard guard when the send is useless without a specific attribute — better than sending a broken message.
- Can Braze Liquid pull data from an external API?
- Yes, via {% connected_content %}. It fetches an external URL at send time and makes the JSON response available as template variables. Always set :cache_max_age to avoid hammering your API, :retry for transient failures, and include a fallback in the template in case the API is unavailable.
- Why does divided_by return an integer?
- Braze Liquid (following Shopify Liquid) returns integer division by default when both operands are integers. To get floating-point results, divide by a decimal literal: | divided_by: 2.0. Or chain with | round: N to control decimal precision.
- Where can I test Liquid without sending real emails?
- The Orbit Liquid Syntax Generator handles simple cases. For complex logic, Braze's campaign preview tool lets you select specific user profiles and see rendered output. For multi-filter chains, prototype in test emails to yourself — preview doesn't catch every edge case, especially around time zones and missing attributes.