Automate email from events
Event-triggered workflows that wait, send, branch on engagement or contact data and update contacts, with versions, runs and step history, through the API and the console builder.
An automation is a trigger, an optional entry rule and a graph of steps. When the trigger fires for a contact, a run starts: it walks the steps one at a time, waits where the graph waits, and keeps a history of every step it took. Each run belongs to one contact and finishes on the version of the automation it started on. Build automations in the console (Automations, a visual builder with the runs beside it) or with the API.
Build one
curl https://api.minket.com/v1/automations \
-H "Authorization: Bearer $MINKET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Order follow-up",
"definition": {
"trigger": { "kind": "custom_event", "event": "order.paid" },
"filter": { "all": [{ "field": "unsubscribed", "op": "eq", "value": false }] },
"allow_reentry": true,
"start": "receipt",
"steps": [
{ "id": "receipt", "type": "send_email", "template_id": "receipt", "variables": { "order": "{{event.payload.order_id}}" }, "next": "pause" },
{ "id": "pause", "type": "wait", "duration": { "amount": 3, "unit": "days" }, "next": "opened" },
{ "id": "opened", "type": "branch", "engagement": { "event": "email.opened", "step_id": "receipt" }, "yes": "tag", "no": "nudge" },
{ "id": "nudge", "type": "send_email", "template_id": "review-request", "next": "tag" },
{ "id": "tag", "type": "update_contact", "properties": { "last_order": "{{event.payload.order_id}}" }, "next": null }
]
}
}'It is created as a draft. Publishing turns it on; from then on every order.paid event for a contact starts a run:
curl -X POST https://api.minket.com/v1/automations/019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a90/publish \
-H "Authorization: Bearer $MINKET_API_KEY"
curl https://api.minket.com/v1/contacts/events \
-H "Authorization: Bearer $MINKET_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "event": "order.paid", "email": "jane@example.com", "payload": { "order_id": "A-1017" } }'await minket.automations.publish("019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a90");
await minket.contacts.sendEvent({
event: "order.paid",
email: "jane@example.com",
payload: { order_id: "A-1017" },
});The event answers 202 { "object": "event", "event": "order.paid", "runs_started": 1 }.
Triggers
kind |
A run starts when |
|---|---|
custom_event |
Your app sends the named event for a contact (POST /v1/contacts/events). |
contact_created |
A contact is created. |
contact_updated |
A contact changes; with properties, only when one of those keys gets a new value. |
topic_subscribed |
A contact chooses to receive topic_id (an opt-in recorded in the consent ledger). |
segment_entered |
A created or changed contact is in segment_id. With re-entry on, it starts again only after the contact was seen outside the segment. |
email_event |
An email event of type (email.delivered, email.opened, email.bounced, ...) happens on mail to a contact, optionally only for mail with a tag { "name", "value" }. |
filter is an entry rule in the segment rule format: a contact who does not match starts no run. A contact is in at most one run of an automation at a time (one live and one test-mode run: a test key's run never holds back a live one, nor counts against its daily cap); allow_reentry lets them start again once their run has ended.
Test keys read automations and their runs but never change them (API keys), since a published automation runs on live events. A test key's events start test-mode runs, and wake only test-mode runs waiting for them: a live run never moves on a test key's event. A test-mode run's mail is test mail (it lands in the sandbox), its contact, topic, segment and delete steps change nothing (they are recorded with the output { "test_mode": true }), and only test keys and the console see it.
Steps
type |
Fields | Moves to |
|---|---|---|
send_email |
template_id (an id or alias; its published version is sent), variables, optional subject, from, reply_to overrides, stream (else the from domain's), topic_id |
next |
wait |
duration: { "amount": 3,, in minutes, hours or days, up to 90 days |
next |
wait_for_event |
event: email.opened or email.clicked (an email this run sent), or custom_event with custom_event naming it (an event for the contact); timeout |
next when it happens, timeout_next when the timeout passes first |
branch |
condition (segment rules on the contact's fields, properties and topics) or engagement: { "event": "email. (one send step's email, or any email of the run when step_id is null; a click counts as an open) |
yes or no |
update_contact |
any of first_name, last_name, unsubscribed, properties |
next |
segment |
action (add or remove), segment_id of a static segment |
next |
topic |
action (subscribe or unsubscribe), topic_id |
next |
delete_contact |
ends the run | |
exit |
ends the run |
A next of null ends the run there. Step ids use letters, digits, _ and - and are unique within the definition; a definition holds up to 100 steps.
Values in variables, the overrides and update_contact may name what the run knows: {{contact.email}}, {{contact.first_name}}, {{contact.last_name}}, {{contact.properties.plan}}, {{event.name}} and {{event.payload.order_id}} (any path into the payload). A value that is exactly one placeholder keeps its type, so "{{event.payload.total}}" stays a number. The template's own variables are filled from the contact (Variables), and a step's variables win over the contact's. A custom event's payload is at most 16 KB of JSON. For email-event triggers the payload holds the email id and its tags only, never the subject or the addresses.
Every step goes through the services every other caller uses. Sends go through the send service, so consent, suppressions, topics, the kill switch, the daily cap and the content checks all apply: an unsubscribed contact's marketing mail is accepted as suppressed and the run goes on. Marketing mail gets the contact's unsubscribe and preferences links in its footer, as a broadcast's does, so its template needs no unsubscribe link of its own. A send refused for a reason that passes (the daily cap, a paused project) waits and is tried again about every ten minutes; any other refusal fails the run. Sends are idempotent per run and step, so a step that runs twice sends once. The consent ledger records the automation's contact, segment and topic changes with the source automation, and their contact.updated events say which run made them.
Versions
Saving a definition (PATCH /v1/automations/{id} with definition) changes the draft; once a version is published, the next save starts a new version. POST /v1/automations/{id}/publish freezes the current version and makes it the one new runs start on. Runs in flight keep the version they started on, to the end. GET /v1/automations/{id}/versions lists them, newest first, with published_at.
Publishing checks what the schema cannot: every step is reachable from start, no steps loop back without a wait between them, and the templates, segments and topics it names exist (templates published, segments static). It answers 422 validation_error with an issue per problem, path naming the step (steps.nudge). The console shows the same issues on the nodes while you build.
POST /v1/automations/{id}/stop (or PATCH with status: "paused") turns it off and stops its running and waiting runs: they schedule no further step. DELETE /v1/automations/{id} archives it the same way. status: "active" turns it on again.
POST /v1/automations/{id}/duplicate makes a new draft named <name> (copy) (cut to 200 characters) from the current version, the newest draft if it is newer than the published one, and answers 201 with the copy. Runs and the published version stay with the original, so the copy does nothing until it is published.
Runs
GET /v1/automations/{id}/runs?status=&contact_id= lists runs newest first; GET /v1/automations/{id}/runs/{runId} returns one with its steps, one entry per step it took (status, started_at, finished_at, output such as the email_id a send made, and error).
Run status |
Means |
|---|---|
running |
Executing steps. |
waiting |
In a wait or wait_for_event until wait_until. |
completed |
Reached the end of its path. |
exited |
Reached an exit step, or its contact was deleted. |
failed |
A step could not be done; error says why. |
stopped |
Its automation was stopped or archived. |
Runs report their end through automation.completed and automation.failed events (payloads). A wake-up that a run has moved past (woken by its event, stopped, waiting again) does nothing.
Runaway guard
- A contact starts at most 50 runs of one automation per day.
- A run takes at most 500 steps; a loop through a wait ends there.
- A loop without a wait is refused at publishing, and a run that comes back to a step without waiting in between fails.
- An automation's own changes never start it again, and runs started by another automation's changes stop three automations deep.