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 toaudienceId. Contacts added by an import are left out unlessincludeImportedis true.form_submitted: a person signs up through the formformId. With double opt-in, they enter when they confirm.event: your code sends an event namedeventName. Up to 5filterson its properties narrow who enters.date: a day arrives for each member ofaudienceId.fieldisbirthdayorjoined, the anniversary of the day they joined that audience.offsetDaysmoves 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.
| 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, or for an event they have to do, with 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, or an event they did within withinDays |
| 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 |
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>, 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.pausedReasonsays why:manual, or a problem the engine met, such assender_refusedortemplate_unavailable.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), 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 the automations that mail that list, 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
pausedReasonsays why.
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.
Who can do what
- Reading needs
automations:readand changing needsautomations:write. Publishing, resuming and sending a test also needemails:send, because they make the automation send mail. - Sending an event needs
contacts:write, and reading a contact’s events needscontacts: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.
automation.entered, automation.exited and automation.paused webhooks tell your systems who went in, who came out and when an automation stopped. 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 openemail.automations and openemail.events, and in the CLI as openemail automations and openemail events. The MCP server has automation tools, so an agent can build one, publish it and watch who is inside.