Skip to main content
Campaign content is written once and rendered per recipient. The template builder uses a familiar dynamic-tag syntax, close to Django and Klaviyo, with three layers that work together:
  • {{ ... }} outputs a value, optionally passed through filters.
  • {% ... %} runs logic: conditionals, loops, and special tags.
  • [Bracket] placeholders from templates you paste in from another tool still work.
Everything below applies to automation send_email and send_inapp content too. It’s one rendering engine across campaigns and automations.

Variables

Reference a value with double braces. Values come from namespaces:

person: the contact

Field names accept snake_case or camelCase, so person.first_name and person.firstName are the same.
Use person.greeting_name in salutations. It falls back through first name, ENS, short wallet, and finally the literal there, so Hi {{ person.greeting_name }}, can never render as Hi ,. A bare person.first_name on a nameless contact renders blank and blocks the send (see Missing values).

person.custom: stored properties

Anything you’ve stored on a contact, including custom event attributes, is available under person.custom:
Custom property names are case-sensitive and matched exactly, so LoyaltyTier must match the stored key. For a property whose name has spaces, use the lookup filter:

organization: your workspace

event: flow and catalog data

event.* holds data supplied by a triggering event, such as cart items or recommended products. It powers catalog loops. In a one-off campaign blast there’s no event, so event.* is empty and any catalog loop renders its {% empty %} branch.

Filters

Pass a value through filters with the pipe |. Chain as many as you like. Filter names are case-insensitive, and an argument uses the colon form.
Unknown filters pass the value through untouched, so a typo like | uppercas silently does nothing. Check rendered output rather than trusting the syntax.

Conditionals

Use {% if %} with {% elif %}, {% else %}, and {% endif %}:
  • Comparisons: ==, !=, >, <, >=, <=, and in (array membership or substring). Operands compare as numbers when both are numeric, otherwise as strings.
  • Boolean logic: and, or, not.
  • Truthiness of a bare value: a non-empty array is true; a blank string, missing value, or numeric 0 is false.
The keywords if, elif, else, endif, for, and endfor are lowercase only. and, or, not, and filter names are case-insensitive.

Loops

Iterate over a list with {% for %}, most often over event data like a cart or a product feed:
Inside the loop, a forloop object is available:
The {% empty %} branch renders when the list is empty or missing. Loops nest, and conditionals, currency, and tag links all work inside them.

Special tags

Currency

{% currency_format %} formats a number as currency (workspace currency, defaulting to USD):

Unsubscribe and preferences

These resolve only at real send time, so they never block a send by being blank in preview. {% tag_link %} renders a link that tags the recipient in your Audience when they click it. It’s how you turn an email into a one-click preference capture (“which chain do you use?”, “pick your favorite collection”) that feeds straight into a segment.
When a recipient clicks, the platform verifies a signature (so the tag can’t be tampered with), applies the tag to their contact (creating it in the Audience if it’s new), records the click, and forwards them to url. The tag is then usable as a segment. You can drive the links from a loop, so a catalog becomes a row of choices:
A tag_link only becomes a real click-to-tag link during an actual send (or a test send). In a plain preview with no delivery, it degrades to just its link text. The url must be http or https.

Imported templates

Templates pasted from another email tool often use square-bracket placeholders like [First Name]. These still work, rewritten to the matching variable before rendering:
  • Matching ignores case, spaces, underscores, and hyphens, so [First Name], [first_name], and [FIRSTNAME] are equivalent.
  • Name-style brackets ([First Name], [Name], [Greeting]) resolve to the blank-safe greeting, never a raw first name.
  • An unrecognized label is left exactly as written, so [Product Name] stays literal rather than becoming a broken variable.
  • A [Label](url) markdown link is left alone.

Missing values block sends

A {{ ... }} variable that resolves blank without a default filter is recorded as a missing field, and missing fields fail validation at launch. A misspelled variable path counts as missing too, since it resolves to nothing.
This is the most common reason a campaign won’t launch. A variable that looks fine in preview, because your test contact has the value, blocks the send if any recipient lacks it. Give every optional variable a fallback:
Or use a built-in blank-safe token like {{ person.greeting_name }}.
Unresolved {% ... %} tags and [Bracket] labels are different: they’re left literal and never block a send. Unsubscribe and preference links are also exempt, since they only resolve during a real send. Your workspace postal address is the one field you should set rather than default, since the compliance footer needs it.

Test sends are tag-aware

A test send now renders against a real delivery, so {% tag_link %} links are live. Clicking a link in a test email tags the actual contact and the tag appears as a segment, letting you confirm click-to-tag end to end before you launch. Test emails are prefixed with [TEST].

Escaping and raw output

Interpolated {{ }} values are HTML-escaped in the email body by default, so recipient data can’t inject markup. Your own template markup is never escaped. Use the raw filter to opt a value out of escaping, and only for content you control.

Writing checklist

  • Subject matches the intent that triggered the send.
  • Preview text supports the primary action rather than repeating the subject.
  • One main call to action.
  • Every optional variable has a default, or uses a blank-safe token like greeting_name.
  • If you use {% tag_link %}, test-click it and confirm the tag lands in the Audience.
  • Links are absolute, and any UTM parameters you care about are set deliberately.