A survey block turns a campaign into a one-question poll. Recipients answer by
clicking an answer directly in their inbox — there is nothing to load, no form
to fill in, and no account to create. Answers land on the campaign's **Survey**
tab, on each subscriber's timeline, and in subscriber filters.

See every style with a screenshot on the [Surveys gallery](/docs/survey).

## Adding a survey

In the campaign editor, open the slash menu (`/`) and pick **Survey**. Each
block holds one question and its answers. One campaign can carry several survey
blocks; each one is tracked separately.

| Style    | What the recipient sees                                            |
| -------- | ------------------------------------------------------------------ |
| Buttons  | Answers stacked as equal-width buttons, labels left-aligned        |
| Yes / No | Two stacked equal-width answers. Switching to it rewrites the first two options as Yes / No |
| Emoji    | Stacked answers prefixed with an emoji you choose                  |
| NPS      | A fixed 0–10 scale, left-aligned with the question                 |

## Block settings

Click the block to open the toolbar.

| Setting           | Values / behavior                                                                 | Default       |
| ----------------- | --------------------------------------------------------------------------------- | ------------- |
| Style             | `buttons` · `yes_no` · `nps` · `emoji`                                            | `buttons`     |
| Comment           | `allowComment` — free-text note after the vote, max 1000 characters               | `false`       |
| Alignment         | `left` · `center` · `right`. Moves the question and the NPS row. Stacked answers stay full width | `left` |
| Button color      | Fill of each answer. Falls back to the theme button color                         | `#000000`     |
| Text color        | Label color inside each answer                                                    | `#ffffff`     |
| Show if           | Tag conditions (`showIfConditions`). Same visibility rules as a Section           | always shown  |
| NPS low caption   | `npsLowLabel` — under 0. `null` uses the recipient's language                     | `null`        |
| NPS high caption  | `npsHighLabel` — under 10. Same fallback                                          | `null`        |

NPS options are virtual (`nps_0` … `nps_10`) and are not stored on the block.
Yes / No only sends the first two options; extra options stay on the block if
you switch back to Buttons.

Colors, alignment and Show if are presentation. Changing them does **not**
create a new survey version. Changing the question, style, answers, comment
toggle or NPS captions does.

## Limits

| Thing            | Limit                                                         |
| ---------------- | ------------------------------------------------------------- |
| Answers          | 2–8 (`buttons`, `yes_no`, `emoji`). NPS is fixed at 0–10      |
| Question         | 1–300 characters, required to send                            |
| Answer label     | 120 characters                                                |
| NPS captions     | 60 characters each                                            |
| Comment          | 1000 characters                                               |
| `surveyId`       | `svy_` + 10 alphanumeric characters, unique in the document   |
| Option `id`      | Stable string, max 64 characters. Never reuse across options  |

The editor can hold an incomplete survey. Sending (schedule, send now, API,
workflow publish) rejects a block with no question, fewer than two labelled
answers, a duplicated id, or merge fields in immutable text.

## TipTap node

Campaigns, workflow emails and the [Tiptap send API](/docs/api-reference/api-emails-tiptap)
all use the same node. Insert it as a top-level block, or inside a section /
column — never inside a `repeat`.

```json
{
  "type": "survey",
  "attrs": {
    "surveyId": "svy_abcdefghij",
    "question": "What should we write about next?",
    "style": "buttons",
    "allowComment": false,
    "alignment": "left",
    "buttonColor": "#000000",
    "textColor": "#ffffff",
    "npsLowLabel": null,
    "npsHighLabel": null,
    "options": [
      { "id": "opt_a1b2c3d4e5", "label": "Talk about AI", "emoji": "🤖" },
      { "id": "opt_f6g7h8i9j0", "label": "No AI please", "emoji": "🙅" }
    ]
  }
}
```

| Attr              | Required | Notes                                                                 |
| ----------------- | -------- | --------------------------------------------------------------------- |
| `surveyId`        | yes      | Mint once. Copying a campaign regenerates it so results do not pool   |
| `question`        | yes      | No `{{variables}}`                                                    |
| `style`           | no       | `buttons` (default), `yes_no`, `nps`, `emoji`                         |
| `options`         | yes*     | 2–8 items with unique `id` + `label`. Ignored for `nps`               |
| `options[].emoji` | no       | Printed only when `style` is `emoji`                                  |
| `allowComment`    | no       | `false` by default                                                    |
| `npsLowLabel`     | no       | Author caption, or `null` to translate                                |
| `npsHighLabel`    | no       | Same                                                                  |
| `alignment`       | no       | `left` (default), `center`, `right`                                   |
| `buttonColor`     | no       | Hex. `null` uses the theme                                            |
| `textColor`       | no       | Hex. `null` is white                                                  |
| `showIfConditions`| no       | Same shape as a Section                                               |

Do not put `{{name}}` (or any merge field) in the question, labels or NPS
captions. Results need one immutable label shared by every recipient.

## What the recipient sees

Clicking an answer opens a small confirmation page and records the answer.
The question and the answers sit in a bordered card — the same chrome as the
editor — so the poll reads as one block in the inbox.
Opening that page never records anything on its own — the answer is only saved
once the page confirms it, so a mail scanner that pre-fetches every link in
your email does not pollute your results.

After answering, the recipient can change their mind: picking another answer
replaces the previous one. Only their latest answer counts. They can add,
edit or clear a comment (empty submit clears it) even without JavaScript.

Test sends use an inert `/s/test` link that records nothing.

## Reading the results

Open the campaign, then the **Survey** tab. For every survey block you get:

- the number of answers, and what share of the delivered emails that is;
- one bar per answer, with its count and percentage;
- for NPS, the score (`% promoters − % detractors`), where 9–10 are promoters,
  7–8 passives and 0–6 detractors;
- the comments, newest first, each next to the answer it came with.

Answers to an option you have since deleted are still counted and shown, marked
as removed. Nothing is ever silently dropped from your totals.

## Automated answers

Some answers are not human — link scanners in corporate mail gateways click
every URL in a message. Lumail flags an answer as automated when it comes from
a known bot user agent, when it arrives implausibly soon after the email was
sent, or when one IP address exceeds 120 answers per minute.

Flagged answers are stored but excluded from every count, percentage and NPS
score, and the results page tells you how many were excluded. Bot-only rows
count as unanswered for filters.

## Filtering subscribers on an answer

In **Subscribers**, add a **Survey** filter and pick a survey.

| Operator             | Matches                                              |
| -------------------- | ---------------------------------------------------- |
| `ANSWERED`           | Anyone who answered this survey                      |
| `NOT_ANSWERED`       | Anyone who did not (including bot-only rows)         |
| `ANSWERED_WITH`      | Current answer is this option (`optionId` required)  |
| `NOT_ANSWERED_WITH`  | Current answer is not this option                    |

Answer identity is stable: renaming an answer, reordering the list, or editing
the question keeps every filter and every recorded answer pointing at the same
option. Deleting an option makes `ANSWERED_WITH` stop matching it.

Filter payload (audience filters, campaign recipient filters, tools):

```json
{
  "type": "SURVEY",
  "field": "survey",
  "surveyId": "svy_abcdefghij",
  "surveyName": "What should we write about next?",
  "operator": "ANSWERED_WITH",
  "optionId": "opt_a1b2c3d4e5",
  "optionLabel": "Talk about AI"
}
```

`surveyName` and `optionLabel` are display-only. Matching uses `surveyId` and
`optionId`.

## Automations

A survey answer emits two things:

- a `SURVEY_ANSWERED` entry on the subscriber's timeline, carrying the question
  and the answer as the recipient saw them;
- a `subscriber.survey.answered` workflow trigger, so a workflow can react to a
  specific answer.

Both fire on the **first** genuine answer only. Changing an answer updates the
result but never re-enrolls the subscriber into a workflow they already went
through.

### Workflow trigger params

On a trigger that listens to `subscriber.survey.answered`:

| Event filter | Required | Effect                                                                 |
| ------------ | -------- | ---------------------------------------------------------------------- |
| *(none)*     | —        | Starts on **every** survey answer in the organization, including surveys created later. Do not leave this empty unless that is the intent |
| `surveyId`   | no       | Restrict to one survey                                                 |
| `optionId`   | no       | Restrict to one answer. Requires `surveyId`                            |

```json
{
  "surveyId": "svy_abcdefghij",
  "optionId": "opt_a1b2c3d4e5"
}
```

Pin the survey. Duplicating a campaign mints a fresh `surveyId` but keeps
option ids, so an unscoped trigger on a copied campaign would not enrol into
the original's workflow — unless you forgot `surveyId` and matched every
survey in the org.

See [Workflow Triggers](/docs/workflows/workflow-triggers).

## Editing a survey after sending

Once a campaign is sent, its survey block is frozen: the question and the
answers can no longer be changed. Everything else about the campaign — its
name, its subject, the rest of its content — stays editable. Recolor and
realign still work; they do not change recorded answers.

To ask a different question, duplicate the campaign; the copy starts a fresh
survey with its own results (`surveyId` is regenerated, option ids are kept).

In a workflow, where the same email keeps going out over time, editing the
survey creates a new version. Older answers stay attached to the version the
recipient actually received, and the results page merges them by answer.

## Related

- [Surveys gallery](/docs/survey) — screenshots of all four styles
- [Send Email in Tiptap](/docs/api-reference/api-emails-tiptap) — `survey` node
- [Workflow Triggers](/docs/workflows/workflow-triggers) — `subscriber.survey.answered`
- [Subscriber Events](/docs/features/subscriber-events) — timeline
- [Language](/docs/features/language) — default NPS captions follow the organization language
