<!-- https://missblue.dev/docs/templates -->

Product guide

# Templates and Liquid

Automation steps, announcements and scheduled messages are Liquid, the same syntax as Shopify and Mailchimp, and they all use the variables below. Each variable is filled in for each person at the moment their message is sent. Saved templates are the ones you keep.

A message typed into a conversation, or sent with `POST /v1/messages`, goes exactly as written: its braces are not filled in.

Liquid, not a bespoke syntax

{% if %}, {% for %}, | upcase and | default: all work the way they do everywhere else.

A missing variable renders as nothing

Never as its own source text. A message reading “Hi {{contact.name}}” does not look like a missing field; it looks like a company that cannot operate its own software.

One renderer

The preview calls the same function the sender calls. A preview with its own copy of the rules eventually disagrees with what goes out.

A template's text is copied

Picking one into a message copies it. Editing the template later cannot rewrite a message somebody already approved and queued.

## Variables

The same list in automation steps, announcements, scheduled messages and saved templates.

-   `contact.name`: their name from your contacts
-   `contact.first_name`: the first word of it
-   `contact.last_name`: the rest of it, after the first word
-   `contact.phone_number`: the number this is going to
-   `contact.custom.<name>`: one of your own fields on their contact, like `contact.custom.checkout_link`. See [Your own fields](#your-own-fields).
-   `system.ampm`, `system.24hr`: the time it sends
-   `system.ddmmyyyy`, `system.mmddyyyy`: the date it sends

Names come from the contact in the project doing the sending, or the organization’s contact when the project has none, and never from another project. A contact saved with a first and a last name is stored as one name, so `first_name` and `last_name` give them back.

Fetch the live list rather than hard-coding it. It comes from the same table the sender reads, so a documented variable cannot resolve to nothing, and it includes every custom field your contacts carry. An API key works here as well as a signed-in session.

```bash
curl https://api.missblue.dev/v1/projects/$PROJECT/templates/variables \
  -H "Authorization: Bearer $MISS_BLUE_API_KEY"
```

```json
{
  "data": [
    { "name": "contact.first_name", "about": "The first word of their name", "kind": "built_in" },
    { "name": "contact.last_name", "about": "The rest of their name, after the first word", "kind": "built_in" },
    { "name": "contact.custom.checkout_link", "about": "A custom field on their contact", "kind": "custom", "field": "checkout_link" },
    { "name": "contact.custom[\"Order number\"]", "about": "A custom field on their contact", "kind": "custom", "field": "Order number" }
  ]
}
```

## Your own fields

Anything you store on a contact can go in a message: the product they left in their cart, the amount, a checkout link, an appointment time. Write it as `{{ contact.custom.product }}`. A field whose name has a space or other characters is written with brackets: `{{ contact.custom["Checkout link"] }}`. Names match exactly, capitals included.

```liquid
Hi {{ contact.first_name | default: "there" }}, your {{ contact.custom.product }} ({{ contact.custom.amount }}) is still waiting: {{ contact.custom.checkout_link }}
```

-   **Read when each message sends.** Not when the automation was saved or the message was scheduled. Change a field before a follow-up goes and the follow-up says the new value.
-   **Missing is nil.** A contact without the field gets the same treatment as one without a name: `| default:` fills it and `{% if contact.custom.product %}` skips it. The preview counts how many people are missing each one.
-   **Plain text.** Values go into the message exactly as stored. Nothing is escaped, so a link with `&` in it arrives intact.

Set them when you save the contact by number. Fields you name are set, the others are kept, and a field sent as `null`, `""` or only spaces is removed. Numbers and `true`/`false` are stored as text. A contact holds up to 50 fields; names are 1 to 80 characters and values up to 2,000. A name cannot contain `}}` or `%}`, or both kinds of quote, because no message could write it. The same `custom_data` works on each row of an import, and a project’s first save of somebody your organization already has keeps the organization’s fields.

```bash
curl -X PUT "https://api.missblue.dev/v1/contacts?project_id=$PROJECT" \
  -H "Authorization: Bearer $MISS_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"handle":"+15555550100","name":"Ada Lovelace","custom_data":{"product":"Blue hoodie","amount":"$48","checkout_link":"https://shop.example/c/91"}}'
```

Or with mb, one `--field` per value:

```bash
mb contact-add --handle +15555550100 --first-name Ada --last-name Lovelace \
  --field product="Blue hoodie" --field amount='$48' \
  --field checkout_link=https://shop.example/c/91
```

For a sequence that uses them, see [From the API, mb or an agent](https://missblue.dev/docs/automations#api) in Automations, and the [Campaigns API reference](https://missblue.dev/docs/campaigns-api).

## Fallbacks

A missing value is nil, not an empty string — which is why `| default:` replaces it and `{% if %}` takes the branch you meant. An empty string is truthy in Liquid and would take the branch written for people who *have* a name.

```liquid
Hi {{ contact.name | default: "there" }}, see you Tuesday.

{% if contact.first_name %}Thanks, {{ contact.first_name }}!{% else %}Thanks!{% endif %}
```

## Preview before you send

Pass the handles you are actually sending to and the response counts how often each variable is empty across them. “38 of 412 have no name” is a decision; “may be empty” is not.

```bash
curl -X POST https://api.missblue.dev/v1/projects/$PROJECT/templates/preview \
  -H "Authorization: Bearer $MISS_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"Hi {{ contact.name }}!","handles":["+13055550142"]}'

{
  "text": "Hi Dana!",
  "error": null,
  "variables": ["contact.name"],
  "unresolved": [],
  "missing": [],
  "counted": 1
}
```

A template that will not parse comes back with `text: null` and one line in `error`. Saving one is refused outright, because the send path has no good option: printing the source is a public failure and refusing is a message nobody receives for a reason they discover later.

## Save one

```bash
curl -X POST https://api.missblue.dev/v1/projects/$PROJECT/templates \
  -H "Authorization: Bearer $MISS_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Appointment reminder","body":"Hi {{ contact.name | default: \"there\" }}, see you tomorrow."}'
```

[Next Scheduled messages Write it now, fill it in at the moment it sends.](https://missblue.dev/docs/scheduled-messages)
