Liquid 101 for Braze: Lesson, Cheat Sheet and Playground

Home / MarTech / Liquid 101
Daniel Robles Interactive lesson Braze

Liquid is an open-source templating language created by Shopify. Braze uses it, with a few additions, in every channel: email, push, SMS, in-app, content cards and webhooks. It personalizes by reading the user profile object (attributes, events, purchases) and catalogs. Learn the essentials, keep the cheat sheet handy, and practice against a sample profile below.

Coming from Salesforce Marketing Cloud? Try the AMPscript 101. Why the two feel so different: Object Oriented Marketing.

1. Lesson 101

Ten concepts cover most real-world Braze Liquid. Every example on the right is live: edit it and the output updates as you type.

What Liquid can see

At send time Braze Liquid sees the whole user profile: standard attributes, custom attributes (including nested objects and arrays), the triggering event's properties, Canvas entry properties, catalogs and, with connected_content, any API. There are no tables to join.

Output and tags

{{ }} outputs a value. {% %} runs logic and outputs nothing by itself. That is the whole syntax.

{% assign greeting = 'Hello' %}
{{ greeting }}, {{${first_name}}}!

Braze attribute syntax

Braze wraps attribute names in ${}: {{${first_name}}} for standard attributes, {{custom_attribute.${plan_tier}}} for custom ones, {{event_properties.${sku}}} for the triggering event. Missing values render as empty, not as an error.

Hi {{${first_name}}}
Tier: {{custom_attribute.${plan_tier}}}
Appointment: {{event_properties.${appointment_id}}}

Variables

assign stores a value; capture stores a rendered block of text.

{% assign tier = custom_attribute.${plan_tier} | upcase %}
{% capture line %}{{${first_name}}} is on {{ tier }}{% endcapture %}
{{ line }}

Filters

Filters transform a value with a pipe and can be chained: upcase, append, replace, plus, date, and Braze additions like number_with_delimiter.

{{ ${first_name} | append: ' ' | append: ${last_name} | upcase }}
{{ custom_attribute.${points} | number_with_delimiter }}

Fallbacks

default handles empty and missing values in one step. Use it on every attribute that could be blank.

Hi {{${first_name} | default: 'there'}},

Conditionals

if / elsif / else / endif, plus unless and case / when. Comparisons are case-sensitive. blank matches empty and missing values.

{% case custom_attribute.${plan_tier} %}
{% when 'vip' %}Your VIP concierge is ready.
{% when 'premium' %}Your premium perks are waiting.
{% else %}Upgrade today.
{% endcase %}

Loops

for loops over arrays on the profile, with limit, offset, reversed and forloop.index.

{% for order in custom_attribute.${recent_orders} limit: 3 %}
{{ forloop.index }}. {{ order.order_id }}
{% endfor %}

Catalogs

catalog_items fetches items by ID from a Braze catalog and stores them in items. IDs can come from the profile or the event.

{% catalog_items products {{custom_attribute.${last_sku}}} %}
Still thinking about {{ items[0].name }}? It is ${{ items[0].price }}.

Connected Content

connected_content calls an API at send time and saves the JSON response to a variable. In this playground two example URLs return mock data.

{% connected_content https://api.example.com/loyalty/{{${user_id}}} :save loyalty %}
You are {{ loyalty.tier }} tier.

Aborting a message

abort_message skips this user instead of sending a broken message. The reason shows up in Braze's message activity log.

{% if custom_attribute.${last_sku} == blank %}
  {% abort_message('No SKU on profile') %}
{% endif %}

Common mistakes

  • Forgetting default. A missing first name renders "Hi ," without an error.
  • Comparing with different case. "VIP" == "vip" is false.
  • Using elseif or else if. Liquid spells it elsif.
  • Integer division. {{ 7 | divided_by: 2 }} is 3; use divided_by: 2.0 for 3.5.
  • Writing {{first_name}} instead of {{${first_name}}} in Braze.
  • Storing changing facts in Canvas entry properties. They are captured at entry; keep live data on the profile.

2. Playground

Your code runs against one sample profile stored the way Braze stores it: a single user object plus catalogs. Change the profile to test your fallbacks. Exercises are graded automatically, and the right panel shows the same task in AMPscript so you can feel the difference.

Liquid Braze
Rendered output
Same task in AMPscript open the AMPscript lesson

                      
What Liquid sees: one profile object plus catalogs

This playground runs LiquidJS with simulated Braze tags (catalog_items, connected_content with mock responses, abort_message) and Braze filters. It is close to Braze, not identical. Dates use your browser's time zone.

3. Liquid cheat sheet

TaskSyntaxNotes
Output{{ value }}
Logic{% … %}
Standard attribute{{${first_name}}} {{${email_address}}} {{${user_id}}}Braze-specific ${} form
Custom attribute{{custom_attribute.${plan_tier}}}Nested objects and arrays supported
Triggering event{{event_properties.${appointment_id}}}Action-based sends
Canvas context{{canvas_entry_properties.${appointment_id}}}Captured at entry
Variables{% assign x = 'a' %}
{% capture x %}…{% endcapture %}
If / else{% if a == b %} … {% elsif %} … {% else %} … {% endif %}Also unless and case / when
Logicand or == != > contains blankCase-sensitive comparisons
Loop{% for o in list limit: 3 %} … {% endfor %}forloop.index, offset:, reversed
Fallback{{ x | default: 'there' }}Covers empty and missing
String filtersupcase downcase capitalize append prepend replace split slice truncate strip
Math filtersplus minus times divided_by round ceil floordivided_by on integers rounds down
Array filterssize first last join sort map where
Dates{{ d | date: '%A, %B %-d' }}
{{ 'now' | date: '%Y-%m-%d' }}
strftime patterns
Braze filtersnumber_with_delimiter url_param_escape json_escape property_accessorBraze additions to standard Liquid
Catalog lookup{% catalog_items products sku_44 %}
{{ items[0].name }}
IDs can come from Liquid: {{custom_attribute.${last_sku}}}
External data{% connected_content https://… :save r %}
{{ r.field }}
Mocked here for two example URLs
Skip this user{% abort_message('reason') %}
Reusable content{{content_blocks.${footer}}}Not in the playground

Official reference: Braze Liquid documentation. Compare with the AMPscript cheat sheet.