---
title: "Personalization & Expressions"
description: "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.
```

<Callout type="warn" title="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.
</Callout>

**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`, `ne` compare as-is, and work when the value is already a number (typical of automation data like `confidence`).
- **Coercing**: `gtf`, `ltf`, `gef`, `lef` read both sides as numbers first. Use these when a value might be text (typical of contact custom fields).

<Callout title="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.
</Callout>

```
{{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}.
```

<Callout type="warn" title="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.
</Callout>

## 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`/`lt` to `gtf`/`ltf`, or wrap the value in `num`.
- 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.
