Automations
Multi-step email sequences that a person is enrolled in once and that end themselves on unsubscribe, reply, suppression, topic opt-out or a tag rule.
An automation is a sequence of up to twenty emails, each sent a set number of minutes after the one before. You define it once, then enrol people by API, by an event, by adding them to an audience, or by tagging them. Prefer it over scheduling several emails yourself: an enrolment ends on its own when the person unsubscribes, replies, is suppressed, opts out of the automation's topic, or their tags say so, which you would otherwise have to track and cancel by hand. The Automations guide walks through a complete sequence.
Every step is sent as marketing mail, with the unsubscribe footer that brings, so an automation's from must be on a verified marketing sending domain. A new automation is a draft and sends nothing until its status is set to active.
The automation object
| Field | Type | Description |
|---|---|---|
id | string | The automation's id (a UUID). Every path below takes this, not the name or slug. |
name | string | Display name. |
slug | string | Lower-case letters, digits and hyphens. Unique within the workspace. Used as the automation tag on every message the automation sends. |
status | "draft" | "active" | "paused" | Only active automations enrol anyone or send steps. |
trigger | object | What starts an enrolment. See Triggers. |
trigger.kind | "api" | "contact_added" | "event" | "tag_added" | |
trigger.audience_id | string | null | The audience a contact_added trigger watches. null for other kinds. |
trigger.event_name | string | null | The event name an event trigger waits for. null for other kinds. |
trigger.tag | string | null | The tag a tag_added trigger waits for, normalised. null for other kinds. |
identity_id | string | The marketing sending domain the steps are sent from. |
from | string | The From address, bare or as Name <address>. |
reply_to | string | null | Where replies go instead of from. When set to an address SendRaven does not receive for, a reply cannot end the sequence. |
steps | array of object | The steps, in order. See Steps. |
exit_on_reply | boolean | Whether a message from the enrolled address ends their enrolment. |
topic_key | string | null | The topic every step is sent under. |
exit_tags | array of string | Contact tags that end an enrolment. Empty when none. |
required_tags | array of string | Contact tags a person must carry to stay enrolled. Empty when none. |
reenrollment | "after_completion" | "never" | Whether someone can go through the sequence again. after_completion: once their enrolment has ended (completed, cancelled or failed) they can be enrolled again, from the first step. never: each address is enrolled once, ever, by any trigger. Automations created before this setting existed read as after_completion. |
default_variables | object | Template values every enrolment gets, under whatever an enrolment passes in variables. {} when none. Keys are returned as written. See Default variables. |
created_at | string (ISO 8601) | |
updated_at | string (ISO 8601) | null | When the automation was last changed, including its status. null until the first change. |
enrollments | object | Enrolment counts by status: active, completed, canceled and failed, each an integer. Every status is always present, 0 when it has none. |
Create, list, retrieve, update and Change an automation's status all return this object, with the same fields. The enrolments themselves are a list of their own: List an automation's enrolments.
Triggers
| Kind | Starts an enrolment when |
|---|---|
api | You call Enrol someone. |
contact_added | Someone joins audience_id who was not already on it, added one at a time. A batch import starts it only when asked to. See Contacts. |
event | Record an event is called with a name equal to event_name. The match is exact and case-sensitive. |
tag_added | A contact gains tag: through Add or remove tags, a contact added one at a time carrying it, a dashboard edit, or a batch import with ?trigger_automations=true. A contact that already carried the tag starts nothing, so a sync that re-sends every contact's tags on every run is safe. |
The enrol endpoint accepts any active automation whatever its trigger kind, so a contact_added, event or tag_added automation can also be enrolled into directly.
Reach for tag_added when an audience holds everyone and a tag says who the sequence is for. With one audience of every signup, a contact_added welcome for one product would go to all of them; a tag_added one on that product's tag goes only to the people who use it. A tag carried before the automation became active starts nothing, so turning one on does not mail your existing list.
A key that could not enrol the address itself (one that holds sends for approval, or a recipient allowlist that leaves the address out) still adds the tag, and starts nothing.
Default variables
default_variables gives every enrolment the same template values, and whatever an enrolment passes in variables wins for the same name. They are read when each step is sent, so a corrected value reaches people already enrolled from their next step.
A contact_added or tag_added trigger enrols people with no values of its own, so every placeholder its steps use, in their subjects, their html and their templates, must have a default. Create refuses one that does not with 422 missing_variables and a missing array, rather than accepting a sequence whose every enrolment would fail at that step. api and event automations are not checked, because their values arrive with each enrolment.
Steps
| Field | Type | Description |
|---|---|---|
delay_minutes | integer | Minutes to wait: from enrolment for the first step, from the previous step being sent for the rest. 4320 is three days. |
subject | string | The subject line. {{name}} placeholders are filled from the enrolment's variables. |
html | string | null | An inline body. Placeholders are filled from the enrolment's variables, with values HTML-escaped. |
template_slug | string | null | A stored template, rendered with the enrolment's variables. |
Give each step html, template_slug, or both; when both are set the template's HTML body wins. Placeholders work the same way in every part of a step as they do in a template: values are HTML-escaped in a body and inserted as given in the subject, and every placeholder needs a value. The step's own subject is always the one sent, so a template's subject is not used by an automation. Only the template's HTML body is sent; its text body is not, although its placeholders still need values. Steps are read from the automation each time one comes due, so a template edit reaches people already partway through.
A step that sends nothing does not move the enrolment on. When the address's domain cannot receive mail (no mail server for it), the message is recorded in the email log as rejected and the enrolment ends failed, with last_error naming the step and undeliverable_domain. When the address was suppressed or opted out of the automation's topic in the moment between the exit check and the send, the enrolment is cancelled with cancel_reason suppressed or unsubscribed_from_topic, as the exit would have done.
A step that cannot be built marks that person's enrolment failed, with the reason in last_error, rather than retrying: a template slug that has since been deleted, a placeholder in the subject, body or template that the enrolment has no value for, a topic that has since been deleted, or the automation having been deleted. A send refused because the workspace is suspended, because from no longer has a verified marketing domain, or because the request is invalid also marks it failed.
Any other refused send is retried about every five minutes, and the reason is written to last_error while the enrolment stays active. Two refusals do not clear by waiting and are given about a day: the workspace's plan allowance being reached, and the workspace having no postal address set, which marketing mail requires. Once the step has been attempted 288 times for one of those, the enrolment is marked failed with the reason. Provider throttling, other provider errors and the sending domain's daily warm-up allowance being used up (it resets at 00:00 UTC) are retried without a limit. Each retry after a warm-up or provider refusal records another message in the email log, queued or failed.
Automatic exits
An enrolment ends on its own for five reasons. Suppression, topic opt-out and the tag rules are checked when the person is enrolled and again before every step, so a change made by any path, including a CSV import or the preference centre, stops the next step. An unsubscribe also writes a marketing suppression, so it is caught the same way. A reply is acted on only at the moment it arrives, and an earlier reply does not keep anyone out. Most exits also cancel the enrolment immediately when they happen, so the dashboard shows it ended at once.
| Exit | cancel_reason | When |
|---|---|---|
| Unsubscribe | unsubscribed | The person unsubscribes from all marketing mail: the one-click link on a message with no topic, "unsubscribe from everything" on the preference page, or a contact marked unsubscribed with PATCH /v1/contacts/{id} or in the dashboard. Also a contact imported with status: "unsubscribed", or a suppression added with reason: "unsubscribe". Every active enrolment for the address in the workspace ends immediately. |
| Reply | replied | An inbound message written by the enrolled person reaches the workspace, for every automation with exit_on_reply. Ends immediately. The message has to arrive on a domain SendRaven receives for, so an automation whose reply_to points elsewhere never sees it. Automatic mail does not count: an out-of-office or other auto-reply, a bounce or delivery report, or anything sent from a mailer-daemon or postmaster address leaves the enrolment running. |
| Suppression | suppressed | The address is suppressed for marketing or for all mail: a hard bounce, a complaint, an unsubscribe, or a suppression added through the API. Caught before the next step. A contact imported with status: "bounced" or "complained" ends it immediately. |
| Topic opt-out | unsubscribed_from_topic | For an automation with a topic_key, the person opts out of that topic: the step's own unsubscribe link, the preference page, set preferences, or an opt-out preference import. Ends immediately. |
| Tag rule | exit_tag or required_tag_missing | The person's contact gains one of exit_tags, or loses one of required_tags. Ends immediately when the tags change through the contact tags endpoint or the dashboard's contact editor; tags set by creating or importing a contact are caught before the next step. Someone with no contact record has no tags: no exit tag applies to them, and they never satisfy required_tags. |
Tags are matched on the person's contact in the workspace by email address. Pausing an automation is not an exit: its enrolments stay active and continue when it is activated again (see Change an automation's status). Deleting an automation cancels its active enrolments with manual and then deletes them, so that reason is never visible through the API.
The enrolment object
Returned by List an automation's enrolments and Enrol someone.
| Field | Type | Description |
|---|---|---|
id | string | The enrolment's id. |
automation_id | string | |
email | string | The enrolled address, lower-cased and trimmed. |
current_step | integer | Zero-based index of the next step to send. |
status | "active" | "completed" | "canceled" | "failed" | |
next_due_at | string (ISO 8601) | When the next step is due. While a step is being attempted it is moved five minutes ahead, which is when a refused send is retried. On an automation that is not active it can be in the past: the step is held until the automation is activated again. Not cleared when the enrolment ends. |
variables | object | The values given at enrolment, for templated steps. {} when none. |
enrolled_at | string (ISO 8601) | |
completed_at | string (ISO 8601) | null | Set when the last step has been sent. |
canceled_at | string (ISO 8601) | null | |
cancel_reason | string | null | "unsubscribed", "unsubscribed_from_topic", "replied", "suppressed", "exit_tag", "required_tag_missing", "manual" or "automation_paused". automation_paused is historical: pausing an automation cancelled its enrolments with it until 15 Sep 2026, and nothing sets it now. |
last_error | string | null | Why the enrolment failed, or, while status is still active, why the last attempt at the current step was refused and is being retried. Cleared when the step is sent. |
attempts | integer | Attempts at the current step, counted when each attempt starts. Reset to 0 when a step is sent and another follows. |
List automations
GET /v1/automations · requires broadcasts:read
Returns a cursor-paged list of automation objects, newest first, each with its enrollments counts.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Page size, at most 100. A larger value is lowered to 100; zero, a negative or a non-number gives 50. |
cursor | string | none | The next_cursor from the previous page. |
Example
Response
200 OK
Create an automation
POST /v1/automations · requires broadcasts:write
Creates an automation as a draft. Nothing is sent and nobody can be enrolled until you change its status to active.
The definition is checked here rather than at the first send, so a mistake is a 422 now instead of a failed enrolment per person later. from must be on a domain that is verified in this workspace as a marketing sending domain, matched exactly on the part after the @ (news.example.com, not example.com), and identity_id must be that domain's id from GET /v1/domains, resolved like every other id in a body: a domain name passed instead is refused with the id it should have been. A topic_key must name an existing topic, a contact_added trigger's audience_id must be an audience in this workspace, a tag_added trigger must name a tag, and every step must have an html body or a template_slug naming an existing template. identity_id, topic_key, trigger.audience_id and each template_slug are trimmed before they are checked and stored.
For an api or event trigger, placeholder values are not checked here, because they arrive with each enrolment: a missing value fails the enrolment when the step comes due. A contact_added or tag_added trigger enrols with none, so default_variables must cover every placeholder (see Default variables).
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | 1 to 200 characters. | |
slug | string | yes | Lower-case letters, digits and hyphens. Must be unused in the workspace. | |
identity_id | string | yes | The id of the verified marketing sending domain that from is on. | |
from | string | yes | At least 3 characters. A bare address or Name <address>. | |
reply_to | string | no | none | 3 to 320 characters. Replies go here instead of from. Set it to an address SendRaven does not receive for and a reply can no longer end the sequence, whatever exit_on_reply says. |
trigger | object | yes | ||
trigger.kind | "api" | "contact_added" | "event" | "tag_added" | yes | See Triggers. | |
trigger.audience_id | string | for contact_added | none | The audience's id, not its name. Ignored for other kinds. |
trigger.event_name | string | for event | none | For example trial_started. 1 to 120 characters, the same limit as the name Record an event accepts. Stored as given, without trimming, and matched exactly. Ignored for other kinds. |
trigger.tag | string | for tag_added | none | 1 to 60 characters, for example audio-player. Normalised as contact tags are, so "Audio Player" is stored and matched as audio-player. Ignored for other kinds. |
steps | array of object | yes | 1 to 20 steps. | |
steps[].delay_minutes | integer | yes | 0 to 525600 (365 days). | |
steps[].subject | string | yes | 1 to 998 characters. | |
steps[].html | string | one of the two | none | Inline body. May contain placeholders. |
steps[].template_slug | string | one of the two | none | An existing template's slug. |
exit_on_reply | boolean | no | true | Turn off for a sequence that should continue after a reply, such as dunning. |
topic_key | string | no | none | An existing topic's key. Every step is sent under it, so its unsubscribe link opts the person out of this topic only. |
exit_tags | array of string | no | [] | Up to 20 tags, each 1 to 60 characters. |
required_tags | array of string | no | [] | Up to 20 tags, each 1 to 60 characters. |
reenrollment | "after_completion" | "never" | no | "after_completion" | Whether an address whose enrolment has ended can be enrolled again. See the automation object. |
default_variables | object | no | {} | Up to 100 names, each letters, digits, _ and ., with string values up to 10,000 characters. Required to cover every placeholder for contact_added and tag_added. See Default variables. |
Tags are normalised as contact tags are: trimmed, lower-cased, runs of whitespace turned into hyphens, and deduplicated, and a tag that is only whitespace is dropped. "Paid Customer" is stored as paid-customer. The 60-character limit applies to each tag as sent: a longer one is refused with 422, not shortened.
Example
Response
201 Created
An automation object. Its enrollments counts are all 0.
Errors
| Status | Type | When |
|---|---|---|
| 409 | conflict | slug is already used by another automation in this workspace. |
| 422 | invalid_request | The body failed validation (details lists each field); a contact_added trigger has no audience_id; an event trigger has no event_name; trigger.audience_id is not an audience in this workspace (when it matches an audience's name, the message gives its id); a step has neither html nor template_slug, or its template_slug names no template in this workspace (the message names the step, for example steps.0.template_slug); or identity_id is not a sending domain in this workspace, is a transactional one, or is not the domain from resolves to. Every identity_id message starts identity_id:; when the value is a domain name rather than an id (news.example.com), the message says so and gives that domain's id, and when it is the wrong domain, the message gives the id from resolves to. |
| 422 | unknown_topic | topic_key names no topic in this workspace. The message starts topic_key:. |
| 422 | no_verified_identity | The domain in from is not a verified marketing sending domain in this workspace. |
| 422 | missing_variables | A contact_added or tag_added automation's steps use placeholders default_variables does not give. missing names them. |
Retrieve an automation
GET /v1/automations/{id} · requires broadcasts:read
Returns the automation object with its enrollments counts, the same object the list holds for it. To see who is enrolled, use List an automation's enrolments.
Path parameters
| Parameter | Description |
|---|---|
id | The automation's id. Passing its name or slug returns 404 with a message giving the id. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No automation with this id in the workspace. |
Update an automation
PATCH /v1/automations/{id} · requires broadcasts:write
Changes the rules that take someone out of the sequence, topic_key, exit_tags, required_tags and exit_on_reply, reply_to, which belongs with them because it decides whether a reply can end the sequence, reenrollment, which decides who can come back in, and default_variables, which fill the steps. The name, slug, sender, trigger and steps cannot be changed once created; any other field in the body is refused with 422 rather than ignored, so a caller that sends steps learns it did not apply.
Fields left out keep their value. null clears topic_key or reply_to, and an empty array clears a tag list. Tags are normalised as on create. A topic_key is checked only when it differs from the current one.
People already enrolled are judged by the new topic_key, exit_tags and required_tags before their next step, not all at once, so a rule changed by mistake can be put back before anyone's next step comes due. A new exit_on_reply applies to the next reply that arrives, a new reply_to to each person's next step, a new default_variables to each person's next step, and a new reenrollment to the next enrolment: nobody already enrolled is affected, and switching to never also keeps out everyone whose enrolment ended before the change. Every call sets updated_at, even one with an empty body.
Path parameters
| Parameter | Description |
|---|---|
id | The automation's id. |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
topic_key | string | null | no | unchanged | An existing topic's key, or null to send without a topic. |
exit_tags | array of string | no | unchanged | Up to 20 tags, each 1 to 60 characters. [] clears. |
required_tags | array of string | no | unchanged | Up to 20 tags, each 1 to 60 characters. [] clears. |
exit_on_reply | boolean | no | unchanged | |
reply_to | string | null | no | unchanged | 3 to 320 characters, or null to send replies back to from. |
reenrollment | "after_completion" | "never" | no | unchanged | Whether an address whose enrolment has ended can be enrolled again. |
default_variables | object | no | unchanged | Replaces the whole set; {} clears it. On a contact_added or tag_added automation, a set that leaves a placeholder its steps use without a value is refused with 422 missing_variables, and nothing is changed. |
Example
Response
200 OK
The updated automation object.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No automation with this id in the workspace. |
| 422 | invalid_request | A field failed validation, or the body contains a field that cannot be changed. details lists each problem. |
| 422 | unknown_topic | topic_key names no topic in this workspace. The message starts topic_key:. |
| 422 | missing_variables | On a contact_added or tag_added automation, the new default_variables leave a placeholder its steps use without a value. missing names them. |
Delete an automation
DELETE /v1/automations/{id} · requires broadcasts:write
Cancels every active enrolment, then deletes the automation together with all of its enrolments, including completed, cancelled and failed ones. Messages already sent are unaffected and stay in the email log.
Deleting cannot be undone, and the enrolment history goes with it. To stop a sequence while keeping its record, set its status to paused instead.
Path parameters
| Parameter | Description |
|---|---|
id | The automation's id. |
Example
Response
200 OK
enrollments_canceled is the number of enrolments that were still active.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No automation with this id in the workspace. |
List an automation's enrolments
GET /v1/automations/{id}/enrollments · requires broadcasts:read
Everyone enrolled in the automation, newest enrolment first (by enrolled_at, then id), as enrolment objects. Cursor-paged like every other list (pagination): pass next_cursor back as cursor while has_more is true. Ended enrolments stay listed with their status, so status=canceled and each row's cancel_reason answer who left the sequence and why.
Path parameters
| Parameter | Description |
|---|---|
id | The automation's id. |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | "active" | "completed" | "canceled" | "failed" | none | Only enrolments in this state. Any other value is refused with 422 rather than ignored. |
limit | integer | 50 | Page size, 1 to 100. A larger value returns 100; zero, a negative or a non-number uses 50. |
cursor | string | none | The next_cursor from the previous page, passed back unchanged. An unreadable cursor returns the first page. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No automation with this id in the workspace. |
| 422 | invalid_request | status is not active, completed, canceled or failed. The message starts with status:. |
Enrol someone in an automation
POST /v1/automations/{id}/enroll · requires emails:send
Puts one address into the sequence. The first step is due delay_minutes after enrolment. The address does not need a contact record, although without one it has no tags, so an automation with required_tags will not take it.
The outcome is in the status code and body rather than always an error. Every outcome answers enrolled, reason and enrollment: reason is null when the person was enrolled, and enrollment is null when they were not.
201withenrolled: trueand the newenrollment: the person is enrolled.200withenrolled: falseandreason: "already_enrolled": they already have an active enrolment in this automation. Retrying a call is therefore safe, whateverreenrollmentsays. Once an enrolment has completed, been cancelled or failed, enrolling the same address again starts the sequence from the first step, unlessreenrollmentisnever.200withenrolled: falseandreason: "previously_enrolled": the automation'sreenrollmentisneverand this address has been through it before (its enrolment completed, was cancelled or failed). Addresses are compared lower-cased.200withenrolled: falseand a reason ofsuppressed,unsubscribed_from_topic,exit_tagorrequired_tag_missing: the person is deliberately excluded by one of the automatic exits. This is a correct outcome; do not work around it.409withenrolled: falseand a reason ofnot_active,no_stepsortopic_missing: the automation cannot take anyone until its definition is fixed. It is notactive, has no steps, or itstopic_keynames a topic that has since been deleted. The body carriesenrolled,reasonandenrollmentalongside the usualerrorobject, whose type isinvalid_state.
The checks run in this order: not_active, no_steps, suppressed, topic_missing, unsubscribed_from_topic, then the tag rules, then previously_enrolled (only when the address has no active enrolment), and already_enrolled last. So a paused automation answers 409 even for a suppressed address, a suppressed address answers 200 suppressed even when the topic is missing, and an exclusion is reported in place of already_enrolled.
The calling key's guardrails decide whether it may enrol at all. A key that holds its sends for approval cannot enrol anyone (403 forbidden): the steps are sent later with no approval step, so enrolling would be a way round the hold. A key with a recipient allowlist can enrol only addresses on it (403 recipient_not_allowed). A key's daily limit is not checked, and the steps an enrolment sends later do not count against it.
Path parameters
| Parameter | Description |
|---|---|
id | The automation's id. Passing its name or slug returns 404 with a message giving the id. |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
email | string | yes | A single email address. Stored lower-cased. | |
variables | object | no | {} | String values for the placeholders in the steps' subjects, inline bodies and templates, for example { "name": "Ana" }. Every value must be a string. |
Example
Response
201 Created
200 OK, not enrolled:
Errors
| Status | Type | When |
|---|---|---|
| 403 | forbidden | The calling key holds its sends for approval. |
| 403 | recipient_not_allowed | The calling key has a recipient allowlist and email is not on it. |
| 404 | not_found | No automation with this id in the workspace. |
| 409 | invalid_state | The automation cannot enrol anyone. The body also has enrolled: false, enrollment: null and reason, one of not_active, no_steps or topic_missing, for example { "enrolled": false, "reason": "not_active", "enrollment": null, "error": { "type": "invalid_state", "message": "This automation is not active. Set its status to active before enrolling anyone." } }. |
| 422 | invalid_request | email is missing or not an address, or variables is not an object of strings. The message names each field that failed, and details lists them. |
Change an automation's status
POST /v1/automations/{id}/status · requires broadcasts:write
Sets the status to draft, active or paused. Any status can be set from any other.
active lets the automation enrol people and send steps. Setting anything else pauses it: triggers no longer start enrolments, enrol calls answer 409 invalid_state with reason not_active, and no step is sent. Enrolments in flight are held, not ended: they stay active on their current step, and a step that comes due while the automation is paused waits. Made active again, the automation continues each person from their current step, and steps that came due in the meantime go out from the next run of the worker (every 30 seconds), oldest first, at the usual pace rather than all at once. Every exit still applies while an automation is paused: an unsubscribe, a reply, a topic opt-out or a tag change ends an enrolment at once as usual, and a suppression is caught when the held step would have been sent.
A step's content is read when it is sent, so pausing to correct a step and activating again sends the corrected step to everyone who had not received it yet.
Activating does not re-check the sending domain. If it is no longer verified, each enrolment fails at its next step with the reason in last_error.
Path parameters
| Parameter | Description |
|---|---|
id | The automation's id. |
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
status | "draft" | "active" | "paused" | yes |
Example
Response
200 OK
The automation object after the change, with its enrollments counts: the same object Retrieve an automation returns.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No automation with this id in the workspace. |
| 422 | invalid_request | status is missing or not one of the three values. |