---
title: "How automations work"
description: "An automation mails people one at a time as things happen: somebody joins a list, fills in a form, does something in your product, or has a birthday. You describe the path once and each person walks it at their own pace."
url: "https://nextmail.sh/docs/api/automations/overview"
area: "API"
category: "Mailbox"
---

# How automations work

An automation mails people one at a time as things happen: somebody joins a list, fills in a form, does something in your product, or has a birthday. You describe the path once and each person walks it at their own pace.

## A trigger and a tree of steps

A `definition` has a `trigger`, the `entry` step and a list of `steps`. Each step has a `key` that is unique in the automation: a lower-case letter followed by 2 to 23 lower-case letters or digits. A step names the one that follows it in `next`, and a `branch` names two, `yes` and `no`. `null` ends that path. The steps form a tree, so no step is reached from two places and nothing loops back. An automation holds up to 50 steps, with branches at most 5 deep.

- `audience_joined`: a contact is added to `audienceId`. Contacts added by an import are left out unless `includeImported` is true.
- `form_submitted`: a person signs up through the form `formId`. With double opt-in, they enter when they confirm.
- `event`: your code sends an event named `eventName`. Up to 5 `filters` on its properties narrow who enters.
- `date`: a day arrives for each member of `audienceId`. `field` is `birthday` or `joined`, the anniversary of the day they joined that audience. `offsetDays` moves it by up to a year: negative for days before, positive for days after.
- `manual`: nobody enters by themselves. You add people from the app or with the enroll endpoint.
- `email_received`: somebody writes to the workspace, and the sender becomes the contact. `addresses` and `domains` say where the email has to arrive, and leaving both empty listens on every address. `threadId` narrows it to one conversation, `match` is `any`, `reply` or `new`, and up to 5 `filters` compare `from`, `subject` or `text`.

| Step | What it does |
| --- | --- |
| `send_email` | Sends the published version of `templateId` from `from`, an address of this workspace. `props` fill the template values, and `subject` replaces the template subject and takes merge fields such as `{{firstName\|there}}` |
| `wait` | Holds the person: for a `duration`, `until` the next given weekday and time, for an `event` they have to do, or for a `reply`, until they write back. The last two take a `timeout` after which they carry on anyway |
| `branch` | Asks one question and sends the person down `yes` or `no`: `email_opened` or `email_clicked` for an earlier email step, `in_audience`, a contact `field`, an `event` they did within `withinDays`, `replied` after a wait for a reply, `email_matches` to compare the `from`, `subject` or `text` of their email, or `ai_check`, where the AI answers a yes or no `question` about their email |
| `add_to_audience`, `remove_from_audience` | Changes which audiences the contact is in |
| `update_field` | Writes a value onto the contact |
| `webhook` | Calls one of your webhook endpoints with an `automation.webhook` event |
| `exit` | Ends the path early. It counts as leaving, not as finishing |
| `reply` | Answers the person’s latest email inside its conversation, from the address that received it. `reply` holds your words as `mode` `text` with `text`, or has the AI write them as `mode` `ai` with `instructions`. It needs an `email_received` trigger or a wait for a reply before it |

A value in `props` or `update_field` comes from one of three places: `{ "source": "static", "value": "…" }`, `{ "source": "contact", "field": "firstName" }` for `email`, `name`, `firstName`, `lastName` or `attributes.<key>`, a custom field of the contact, and `{ "source": "event", "path": "orderId" }` for a property of the event that started the run.

## A draft and a live version

Saving changes the draft `definition`. Nothing runs until `POST /automations/{id}/publish` freezes the draft as a numbered version, which `published` then shows. People who are already inside finish on the version they entered with, and people who enter afterwards get the new one. `hasUnpublishedChanges` says the draft has moved on.

- `draft`: never published. Nobody enters.
- `live`: published and running.
- `paused`: nobody enters and everybody inside holds where they are. `pausedReason` says why: `manual`, a problem the engine met, such as `sender_refused` or `template_unavailable`, or `plan_limit` after a downgrade.
- `archived`: retired for good. Everybody inside leaves, and the history stays.

`settings` are separate and apply as soon as they are saved: the `timezone`, a `sendWindow` outside which emails wait, `reentryDays` before the same person may enter again (`null` means once and `0` means straight away), `exitOnLeave` to drop people who leave the trigger audience, and `listAudienceId`, the audience an unsubscribe is recorded on.

`problems` lists what is wrong with the draft, each with a `code`, the `path` of the field, the `stepKey` and whether it is `blocking`. A draft with a blocking problem cannot be published.

## People inside an automation

Each person who enters gets an enrollment. It is `active` while they move through the steps, `completed` when they run off the end of a path, and `exited` when they leave early, with an `exitReason`: `exit_step`, `unsubscribed`, `suppressed`, `left_audience`, `removed`, `archived` or `failed`.

- Steps run within about 15 seconds of becoming due. A person never gets two emails from one automation in the same pass.
- Automation emails are marketing mail, so every one carries an unsubscribe link. A person who unsubscribes leaves every automation that sends them marketing mail and stays out of them, and one whose address bounced or complained leaves at their next step.
- An email whose address cannot receive mail is skipped, and the person carries on to the next step.
- When a send is refused for everybody, such as a sender that lost its domain or a template that was unpublished, the automation pauses and `pausedReason` says why. The workspace owner gets an email naming the automation and what to fix, whenever an automation pauses itself.

## Events from your app

`POST /events` records that a contact did something: `order.placed`, `trial.started`, `plan.upgraded`. An event starts every live automation whose trigger names it, moves on anybody waiting for it, and answers the `event` question of a branch. Events are kept for 90 days.

## Answering email

An automation can start from an email somebody sends you, or stop in the middle of a series until they write back. The steps after that work on their latest email: a `branch` can ask the AI about it or compare its text, and a `reply` step answers inside the same conversation. This one gives a code to anybody who writes to `offers@example.com` and agrees to buy.

**Definition**

```
{
  "trigger": {
    "kind": "email_received",
    "addresses": ["offers@example.com"],
    "domains": [],
    "threadId": null,
    "match": "any",
    "filters": []
  },
  "entry": "agrees",
  "steps": [
    {
      "key": "agrees",
      "kind": "branch",
      "condition": { "kind": "ai_check", "question": "Do they agree to buy?" },
      "yes": "code",
      "no": null
    },
    {
      "key": "code",
      "kind": "reply",
      "next": null,
      "reply": { "mode": "text", "text": "Thanks {{firstName|there}}, your code is SAVE20." }
    }
  ]
}
```

- Set `reentryDays` to `0` in the settings to run it for every email. With `null` each person starts it once.
- Spam, mailing lists, automatic replies and mail from your own addresses never start it, and one person starts the same automation at most 10 times a day.
- A reply is ordinary mail, not marketing mail. It leaves from the address that received the email, is marked as an automatic reply and carries no unsubscribe link.
- An `ai_check` and a reply the AI writes each use one AI action. When the day’s AI actions are used up the person waits, and `heldFor` says `ai_limit`.
- A value can read the email with `{ "source": "event", "path": "email.subject" }`. `email.text`, `email.from` and `email.to` are there too.

## Who can do what

- Reading needs `automations:read` and changing needs `automations:write`. Publishing, resuming and sending a test also need `emails:send`, because they make the automation send mail.
- Sending an event needs `contacts:write`, and reading a contact’s events needs `contacts:read`.
- An API key and the owner see every automation in the workspace. An app a member connected sees the ones that member made.
- Deleting an automation asks an OAuth app for a verification code. An API key never needs one.
- A plan allows 1 live automation on Free, 10 on Starter, 50 on Business and any number on Enterprise. A workspace holds 100 automations by default.
- After a downgrade, the newest live automations beyond the new allowance pause with `pausedReason` `plan_limit`. The oldest keep running. Resume one after you upgrade or pause another.

`automation.entered`, `automation.exited`, `automation.paused` and `automation.resumed` webhooks tell your systems who went in, who came out and when an automation stopped or started again. An endpoint receives them only when it names them in `eventTypes`. Archiving an automation ends everybody in it without an `automation.exited` event for each person.

## From code, the terminal and agents

Everything here is also in the SDK as `nextmail.automations` and `nextmail.events`, and in the CLI as `nextmail automations` and `nextmail events`. The MCP server has automation tools, so an agent can build one, publish it and watch who is inside.
