Merge tags and personalization

How a variable resolves at send time, and in what order.

Pombo uses real Liquid: {{ variable }} for values, and {% if %} for conditional content (see below). In the editor, a {{ variable }} shows as a pill; underneath it's just a variable reference filled in per recipient.

Where values come from

Highest priority wins:

  1. Per-send override. The "Send test" modal in the app, or the variables argument to send_test_email and send_transactional_email.
  2. Event property. {{ event.* }} only, from the payload of the event that triggered the send. A separate namespace: {{ event.amount }} never falls through to a contact field called amount.
  3. Contact field. The recipient's own row, including custom fields.
  4. Template baked value. A variables map saved on the template, for things that are the same for everyone on a given send (an issue number, a campaign code).
  5. Inline default. {{ first_name | default: "there" }} renders "there" when nothing better is available.

Contact fields

{{ first_name }}, {{ last_name }}, {{ email }}, {{ phone }}, and any custom field resolve from the contact. An unfilled tag with no default renders as a visible marker in previews so you notice it.

Event data

A send triggered by POST /api/v1/events carries that event's properties. Read them as {{ event.amount }}, {{ event.receipt_url }}, and so on, plus {{ event.name }} for the event's own name. This works in event-triggered sequences and event-triggered transactional email.

Conditionals: only show something when a value is there

An optional property, a coupon code or a referrer's name, is either there or it isn't, and copy that's only correct one of those ways reads broken the other. Wrap the optional part in {% if %}, and compare against blank:

Thanks for your gift{% if event.amount != blank %} of {{ event.amount }}{% endif %}!

This reads "Thanks for your gift of $50!" when the amount is set, and "Thanks for your gift!", not "Thanks for your gift of !", when it isn't.

Write != blank, not a bare {% if event.amount %}. In Liquid an empty string is true, so a bare {% if %} only catches a property that is missing altogether. One that arrives present but empty, which is what an event property usually is, takes the branch anyway and you get "Thanks for your gift of !". != blank covers empty, missing and null alike.

{% unless %} works the same way. Type the tag text straight into the block editor like any other text; there's no separate toolbar button for it yet.

One limit to know about before you lean on it. The block editor escapes " and > on the way into the saved template, and the escaping does not stop at Liquid's braces, so {% when "gold" %} and {% if event.count > 5 %} are not read the way you typed them. Neither raises; the email just says the wrong thing (#891). Comparisons against blank use neither character, which is why they are the form recommended here. If you need {% case %}, a quoted value or a numeric comparison, write that email as raw MJML, where the text reaches Liquid exactly as typed.

Before you rely on a conditional in production, use "Send test" with the value left blank to see the no-value case. If a value is blank rather than missing entirely, it renders as nothing rather than a [tag] marker, so it's easy to miss a broken sentence by eye, so send_test flags likely leftover punctuation (a stray , ! or similar) so you catch it there.

System tags

{{ unsubscribe_url }} is injected per contact; you don't set it. The footer references it automatically on marketing mail.

Something unclear or missing? Tell us.