Personalization & Expressions
Merge fields, conditionals, fallbacks, functions, spintax, and automation expressions: the templating language shared by campaign emails and automations.
Write one email and have it come out personalized for every contact, using merge fields like {{.FirstName}} plus helpers for fallbacks, conditions, and math. No programming knowledge needed; match the patterns below.
Quick reference
| You want to | Write |
|---|---|
| Insert a first name | {{.FirstName}} |
| Back up an empty field | {{.FirstName | default "there"}} |
| Branch on a condition | {{if eq .Company "Acme"}}...{{else}}...{{end}} |
| Randomize wording (campaigns only) | {a|b|c} |
| Filter an automation by a number | gtf .confidence 0.8 |
Variables
Contact fields (campaigns)
Exact names, capitalized, with a leading dot: {{.FirstName}}, {{.LastName}}, {{.Email}}, {{.Company}}, {{.Phone}}.
Hi {{.FirstName}}, I noticed {{.Company}} is hiring and wanted to reach out.Field names are case-sensitive
{{.FirstName}} works. {{.firstname}}, {{.firstName}}, and {{.FIRSTNAME}} do not, and leave an empty value or a leftover token in your email.
Custom fields work the same way with a dot: {{.industry}}, {{.account_owner}}. Names containing spaces or dashes also work as written ({{if .job title}}...{{end}}) and resolve everywhere, including inside conditions and helpers, because Warmbly rewrites them to an index lookup for you.
Trigger variables (automations)
Automations carry the trigger's data rather than a contact's saved fields, keyed in lowercase and referenced with the required leading dot:
New reply from {{.contact_email}} on campaign {{.campaign_id}} ({{.intent}})| Trigger | Variables |
|---|---|
| Reply received | contact_email, contact_id, campaign_id, intent, confidence, subject, snippet |
| Meeting booked / rescheduled | invitee_name, invitee_email, event_name, scheduled_for, join_url, source, contact_id |
| Meeting canceled | Same, without join_url |
| Email bounced, Deliverability complaint | contact_email, campaign_id, contact_id, event_type, provider, reason |
| Unsubscribed | contact_email, contact_id, campaign_id, source |
| Warmup health changed | email, new_state, previous_state, reason |
| Campaign action | contact_email, contact_id, campaign_id, campaign_name, first_name, last_name, company, phone |
Action text is a full template, not token substitution: conditionals, pipelines, helpers, {{range}}, and nested fields ({{.lead.company}}) all work, and values keep their type. Because numbers stay numbers, {{if gt .confidence 0.8}} works directly in a message body. A malformed template never blocks the action; it falls back to plain {{.key}} substitution. Unknown keys render empty.
Fallbacks
Put the field first, then a pipe, then default and your backup in quotes:
Hi {{.FirstName | default "there"}},
Hope things are going well at {{.Company | default "your company"}}.Conditionals
{{if}} includes text only when something is true, {{else}} handles the other case, and every {{if}} must close with {{end}}.
{{if .Company}}I see you're at {{.Company}}.{{end}}
{{if eq .Company "Acme"}}Great to connect with the Acme team.{{else}}Great to connect.{{end}}eq is equals and ne is not-equals. Quote your text values.
Functions
The same helpers work in campaign emails, automation conditions, and action text.
| Math | Meaning | String | What it does |
|---|---|---|---|
add sub mul div mod | Arithmetic | lower upper title | Change case |
num a | Force a value to a number | trim | Remove surrounding spaces |
contains | Contains, case-insensitive | ||
hasPrefix hasSuffix | Starts or ends with, case-sensitive |
Division or modulo by zero returns 0 rather than erroring, so these never break an email.
Comparing numbers
- Native:
gt,lt,ge,le,eq,necompare as-is, and work when the value is already a number (typical of automation data likeconfidence). - Coercing:
gtf,ltf,gef,lefread both sides as numbers first. Use these when a value might be text (typical of contact custom fields).
Rule of thumb
Comparing automation numbers? Native gt / ge works. Comparing a custom field or anything that might be text? Use the f versions so "42" reads as 42. When in doubt, the f versions are safe.
{{if gtf .deal_size 10000}}Enterprise prospect{{end}}
{{if hasSuffix .Email ".edu"}}As someone in academia, {{end}}I thought this might help.Spintax
Alternatives in single braces separated by pipes, so a batch of sends does not look identical:
{Hi|Hey|Hello} {{.FirstName}}, {hope you're well|hope things are good}.Spintax rules
Campaign subject and body only; it does not run in automations. Use single braces, not the double braces of merge fields. Only groups containing a pipe count, so a brace run without one (CSS or JSON in an HTML email) is left exactly as written.
Advanced automation expressions
For anything the visual picker cannot express. An expression passes when it renders to something truthy; it is false when empty, false, 0, no, off, or <no value>, and a broken expression is false rather than silently passing.
Write it bare (recommended) or as a full template when you need {{else}}:
gt .confidence 0.8
{{if gt .confidence 0.8}}yes{{end}}and (gtf .confidence 0.8) (eq .intent "positive")
or (eq .intent "positive") (eq .intent "neutral")
not (eq .intent "negative")
hasSuffix .contact_email "@acme.com"and, or, and not take whole tests as inputs, so wrap each in parentheses when mixing groups.
Where each thing applies
| Surface | Variables | Conditionals / functions | default | Spintax |
|---|---|---|---|---|
| Campaign subject & body | Contact fields | Yes | Yes | Yes |
| Automation condition | Trigger keys | Yes | Yes | No |
| Automation action text | Trigger keys, with the dot | Yes | Yes | No |
Gotchas
- Standard fields are case-sensitive:
{{.FirstName}}, not{{.firstname}}. {a|b}is spintax (campaigns only);{{...}}is a variable, condition, or function.- Always close
{{if}}with{{end}}, and quote text values. - If a comparison on a custom field behaves oddly, switch
gt/lttogtf/ltf, or wrap the value innum. - Unknown variables render empty rather than erroring. A leftover
{{...}}usually means a typo or wrong capitalization. - In action text keep the leading dot. It is standard Go
text/template, so a bare{{contact_email}}is invalid.
Preview and validation
Campaigns: the composer's live Preview renders against a sample contact exactly as the real send does (variables, conditionals, functions, then spintax). It flags errors like a broken {{if}}, which block launching, and unresolved tokens left after rendering.
Automations: an advanced expression is validated on save, so a condition that does not parse cannot be saved. The Reference button lists every variable and function inline.