Automations
Multi-step email programs triggered by a contact event — steps, waits, branches, and the forward-only rule that makes loops impossible.
An automation is a small program that runs per contact. You define an ordered list of steps — send, wait, branch, jump — and a trigger that enrolls contacts into it. emitd advances each enrollment independently.
Lifecycle
An automation is created as a draft. Only name and trigger_kind are
required up front; everything else can be filled in with PATCH before you
activate:
curl https://api.emitd.com/v1/automations \
-H "Authorization: Bearer $EMITD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Welcome series", "trigger_kind": "contact_created" }'Then activate to start enrolling, pause to stop advancing without losing
state, and archive to retire it:
| Action | Endpoint |
|---|---|
| Activate | POST /v1/automations/{id}/activate |
| Pause | POST /v1/automations/{id}/pause |
| Archive | POST /v1/automations/{id}/archive |
A verified domain is required to activate
from_domain_id must reference one of your verified sending domains before
activate will succeed. Set it with PATCH while the automation is still a
draft — see Sending domains.
Triggers
trigger_kind | Enrolls when | trigger_config |
|---|---|---|
contact_created | A contact is created in your workspace | ignored |
audience_added | A contact is added to a specific audience | {"audience_id": "<id>"} — required |
manual | You call the enroll endpoint yourself | ignored |
An audience_added trigger needs an audience you own — see
Audiences. A trigger_config naming an audience
that isn't yours is rejected with audience_not_found.
Manual enrollment
curl https://api.emitd.com/v1/automations/$ID/enroll \
-H "Authorization: Bearer $EMITD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "contact_id": "con_8Qd21k" }'Enrollment is idempotent: a contact can only be enrolled in a given
automation once, so re-enrolling is a no-op. A genuine enrollment returns
201; an existing one returns 200. You can retry this call safely without
double-mailing anyone.
The automation must be active — enrolling into a draft or paused automation
returns 409 with not_active. A contact_id that doesn't exist or isn't
yours returns 422 with contact_not_found.
Steps
A program is an array of up to 50 steps. type discriminates the shape.
send
Enqueues one marketing send to the enrolled contact, then advances.
subject is required and non-empty, and at least one of body_html /
body_text is required.
{ "type": "send", "subject": "Welcome aboard", "body_html": "<h1>Hello</h1>" }wait
Parks the enrollment for seconds, then advances. Range is 0 to
7776000 — 90 days.
{ "type": "wait", "seconds": 86400 }branch
Evaluates a segment filter against the enrolled
contact. On a match it advances to the next step; otherwise it jumps to
else_to.
{
"type": "branch",
"filter": { "match": "all", "conditions": [
{ "field": "attributes.plan", "op": "eq", "value": "pro" }
]},
"else_to": 4
}goto
Jumps unconditionally to step index to.
{ "type": "goto", "to": 6 }Loops are structurally impossible
Every jump target — branch.else_to and goto.to — must be strictly
forward: greater than the jumping step's own index, and at most the program
length. The length value is a sentinel meaning "end of program".
This is a deliberate design constraint, not a validation gap. It means an automation always terminates, so no enrollment can spin and no contact can be mailed in a cycle. If you are used to drip tools that let you loop back, model the repetition as explicit forward steps instead.
A backward or out-of-range target is rejected with bad_jump_target.
A complete program
A three-day onboarding series that skips the nudge for contacts who already upgraded:
{
"from_domain_id": "dom_4Kq9",
"steps": [
{ "type": "send", "subject": "Welcome to Acme", "body_html": "<h1>You're in.</h1>" },
{ "type": "wait", "seconds": 259200 },
{ "type": "branch",
"filter": { "match": "all", "conditions": [
{ "field": "attributes.plan", "op": "eq", "value": "free" }
]},
"else_to": 5 },
{ "type": "send", "subject": "Ready to upgrade?", "body_html": "<p>Here's what Pro adds.</p>" },
{ "type": "goto", "to": 5 },
{ "type": "send", "subject": "Tips for week two", "body_html": "<p>Three things to try.</p>" }
]
}Step indices are zero-based. The branch at index 2 sends free-plan contacts
on to index 3 (the upgrade nudge) and everyone else to index 5, skipping it.
The goto at index 4 rejoins the main line so the nudged contacts also reach
index 5. Both jumps point forward, so the program is valid.
Validation errors
422 responses carry an error code naming exactly what was rejected:
| Code | Meaning |
|---|---|
invalid_name | name missing or blank |
invalid_trigger_kind | not one of the three kinds |
missing_audience_id | audience_added without trigger_config.audience_id |
audience_not_found | the audience id is not yours |
steps_not_array | steps was not a JSON array |
too_many_steps | more than 50 steps |
step_not_object | a step was not a JSON object |
unknown_step_type | type is not send/wait/branch/goto |
missing_field | a step omitted a required field |
empty_subject | a send step's subject was blank |
missing_body | a send step had neither body_html nor body_text |
wait_out_of_range | seconds outside 0–7776000 |
bad_jump_target | a jump was backward, self-referential, or past the end |
bad_branch_filter | the branch filter failed segment-filter validation |
Listing
GET /v1/automations returns a newest-first keyset page over
(created_at, id). Pass the previous response's next_cursor as cursor to
continue; a null cursor means the last page.
Full schemas: Automations API.