Skip to the documentation
API

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.
StepWhat it does
send_emailSends 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}}
waitHolds 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
branchAsks 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_audienceChanges which audiences the contact is in
update_fieldWrites a value onto the contact
webhookCalls one of your webhook endpoints with an automation.webhook event
exitEnds the path early. It counts as leaving, not as finishing
replyAnswers 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 [email protected] and agrees to buy.

Definition
{  "trigger": {    "kind": "email_received",    "addresses": ["[email protected]"],    "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.