---
title: Conditional email and sent previews
description: Personalize authored HTML with bounded MiniJinja and deliver a test to a chosen inbox.
---

## Authored content, without runtime AI

Put `subject` and either `html` or `text` on each campaign variant; `preheader` is optional. Text alone creates escaped HTML with line breaks. Authored HTML retains its design; when both are supplied, text is the MIME alternative. Set `prompt: null` to use only that authored content. The HTML renderer escapes inserted values. Subject and plain text remain plain strings. See [text, HTML and Zapier](/email-content).

The template language is a bounded subset of **MiniJinja**, not Liquid. Supported paths are `recipient.*`, `sender.*`, `campaign.*` and `variables.*`. See [sender profiles and signatures](/sender-profiles) for the full namespace and inheritance contract. Campaign messages capture the contact at acceptance; sender defaults are refreshed at claim when `live_configuration` is enabled. Otherwise the sender is captured at acceptance; standalone messages use the request's `variables`. Sent previews provide the selected contact and source campaign, with the chosen sender's automatic signature.

```jinja
<p>{% if recipient.given_name | default("") != "" %}
Hola, {{ recipient.given_name | trim | title }}.
{% else %}
Hola, equipo de {{ recipient.custom_fields.institution_display_name | default("su institución") }}.
{% endif %}</p>

<p>{% if recipient.custom_fields.department | default("") == "Bogotá, D.C." %}
Podemos empezar con una sede en Bogotá.
{% elif recipient.custom_fields.municipality | default("") != "" %}
Podemos revisar el proceso en {{ recipient.custom_fields.municipality | title }}.
{% else %}
Podemos empezar con una sola sede.
{% endif %}</p>
```

Use `if`, `elif`, `else`, `endif`, with at most 16 nested conditions. Conditions may check a path's truth value, prefix it with `not`, or compare with `==` / `!=` against a double-quoted JSON string, number, boolean or null. Filters can be chained before comparison. Boolean fields compare with `true`, not the string `"true"`.

| Filter | Behavior |
| --- | --- |
| `default("text")` | Replaces missing, null and empty strings. It preserves false and zero. |
| `has_tag("tag")` | Tests exact membership in `sender.tags`. |
| `has_word("word")` | Tests a whole alphanumeric word in text, ignoring case. Punctuation separates words; accents remain significant. Input is limited to 100,000 UTF-8 bytes and the single word to 128 bytes. |
| `trim` | Removes surrounding whitespace. |
| `lower` | Unicode lowercase. Useful for text, not as a substitute for validating email. |
| `upper` | Unicode uppercase. |
| `title` | Uppercases the first letter after a nonletter and lowercases the remaining letters. |

Use at most eight filters per expression. `title` is useful for names and municipalities; it does not know legal acronyms. Keep a normalized `institution_display_name` field for names such as “Clínica del Norte IPS”. Keep identifiers and phone strings unchanged.

Use existing institution data to select a relevant example without modifying the contact:

```jinja
{% if recipient.custom_fields.institution_display_name | default("") | has_word("hospital") %}
We can review an outpatient follow-up after discharge, if your hospital manages that process.
{% elif recipient.custom_fields.institution_display_name | default("") | has_word("laboratorio") %}
We can review follow-up on an outstanding laboratory order.
{% else %}
We can review one outstanding care activity with your team.
{% endif %}
```

`has_word("IPS")` matches “Centro IPS Norte”, not “Philips”. It does not infer a verified institution type, contract, clinical service or local problem. Use separate branches for accented and unaccented spellings (for example, `clínica` and `clinica`), and a general fallback when the name supplies no reliable signal. More specific signals such as dental or rehabilitation should precede a generic clinic match.

Missing values without `default` fail rendering. Output is limited to 255 subject characters, 300 preheader characters and 100,000 characters per body. Interpreter fuel is 50,000 and recursion depth is 32. Loops, includes, macros, assignment, arbitrary calls, arithmetic, comments, `safe` and disabling escaping are rejected. Data is never reinterpreted as template source. Use `default` on optional fields in conditions too.

## Send a preview using the messages resource

Authored HTML and text also accept one `{{ footer }}` token per alternative for explicit signature placement. If omitted, the sender signature appends automatically. See [signature placement](/sender-profiles#place-the-signature-before-a-postscript) for conditions, escaping, plain text and validation rules.

A preview is a **real standalone email**, accepted by the same durable queue. It uses mailbox pacing, quota, schedule and suppressions. It never enrolls the original person, executes runtime AI, copies a variant's CC/BCC or mailbox BCC, or contributes campaign opens. The test inbox receives a small banner identifying the source person and variant by default. Set `include_metadata: false` for a clean example without that banner; personalization and recipient isolation remain identical.

```json
{
  "preview": true,
  "include_metadata": false,
  "person_id": "per_0195f30ed98470008000000000000001",
  "variant_id": "var_0195f30ed98470008000000000000001",
  "from": "david.lara@example.com",
  "to": "your-test-inbox@example.com"
}
```

POST this body to `/v1/messages` with your workspace API key and a unique `Idempotency-Key`. The person, variant and sender must belong to that workspace. `from` is a connected mailbox email. `to` is one test address and is the only recipient. Poll the returned message location: `202` means queued and `Sent` means SMTP accepted. Inspect the receiving inbox separately to establish receipt.

To test inline content, omit `variant_id` and provide `subject`, either `html` or `text`, and optional `preheader` and `variables`:

```json
{
  "preview": true,
  "person_id": "per_0195f30ed98470008000000000000001",
  "from": "david.lara@example.com",
  "to": "your-test-inbox@example.com",
  "subject": "Hello {{ recipient.given_name | default(\"team\") | title }}",
  "html": "<p>{{ variables.introduction }}</p><p>{{ sender.name }}</p>",
  "variables": { "introduction": "A short, authored introduction." }
}
```

Do not combine a saved variant and inline content. A replay of the same key and request returns the original accepted preview even if the contact or variant changes. Use a new key for an intentional new preview. Standalone preview messages have null campaign/step/variant columns; the request and optional banner identify the source, without polluting campaign measurement. Campaign opt-out and tracking are checked separately with a controlled campaign recipient.
