Build an automation
An automation is a graph (a DAG): a trigger, then wait, condition, action and message nodes. It lives in two states: draft and active version.
1. Look at what is already there
list_automations
Do this always, before creating or activating. It also shows the triggers of
the published version (in triggers: there can be more than one), and that is the
piece of information that prevents damage: triggers like contact.list_added and
contact.tag_added start real sends.
If you add a contact to a list while an active automation is hooked to that list,
you have just sent. The same goes for tag_contacts.
2. Create the draft
The body is a Journey: name, entry (id of the starting node) and nodes. Every
node has an id, a type and the edges to other ids (or null, which ends the
branch).
create_automation {
journey: {
name: "Welcome",
entry: "t1",
nodes: [
{ id: "t1", type: "trigger", event: "contact.list_added",
match: { listId: "<uuid>" }, next: "tag1" },
{ id: "tag1", type: "add_tag", tagIds: ["<uuid>"], next: "w1" },
{ id: "w1", type: "wait", duration: "P1D", next: "m1" },
{ id: "m1", type: "message", preferredChannel: "email",
fallbackChain: ["email"], templateByChannel: { email: "<uuid>" }, next: "done" },
{ id: "done", type: "exit" }
]
}
}
This creates the draft and version 1. Nothing is sent yet.
The uuids of tags, lists, templates and automations come from list_tags,
list_lists and list_automations: do not invent them.
Available nodes
Sending · message (email/WhatsApp/Telegram with fallback, optional
attachmentIds and ai), notify (email to your own team, not to the contact).
Flow · wait (exactly one of duration ISO-8601, until date, or
untilCondition), wait_for_event, branch (onTrue/onFalse), split
(percentages across 2 to 4 branches), goto (jump to another step),
start_automation, end_automation, webhook, goal, exit.
Contact · add_tag, remove_tag, subscribe, unsubscribe, update_contact
(null deletes a key), add_note, score, conversion.
The complete list with the fields of each one is in the description of
create_automation: read it instead of guessing.
Several entry triggers
An automation can start from several events: add more trigger nodes, each with
its own next. The contact enters through the trigger that matches the event, so
two entry points can lead to two different branches or to the same step.
{ id: "t1", type: "trigger", event: "contact.list_added", match: { listId: "<uuid>" }, next: "w1" },
{ id: "t2", type: "trigger", event: "cart.abandoned", next: "w1" },
entry must be the id of a trigger: it is where contacts enrolled by hand start
from, when no event is in play.
Three rules the save enforces
You need at least one trigger, two triggers with the same event and the same
match are refused (the second would add nothing), and a trigger other than the
entry must have a next: a disconnected one would do nothing.
Cycles are allowed only through a goto node (with a ceiling of 50 hops per
contact). A loop made of next edges chasing each other is refused: it is almost
always a writing mistake.
Careful with add_tag and subscribe: they re-emit contact.tag_added and
contact.list_added, so they can start another automation. Check
list_automations before using them.
3. Fix without recreating
get_automation { automationId } → reads back the published Journey
update_automation { automationId, journey } → publishes a new version
Always start from the DAG that get_automation returns: rebuilding one from scratch
loses the parts you did not know about. Contacts already inside stay on the version
they entered with; the change applies to new entries.
4. Activate, but only with the user's consent
activate_automation { automationId }
From here on, every contact that satisfies the trigger enters the flow and receives real messages. Have the user confirm explicitly before calling it: this is the point of no return.
5. Enroll contacts by hand
This is for people who are already there, without waiting for the event to happen again (a recovery campaign, a realignment after an import):
enroll_contacts { automationId, listId } // or segmentId, or contactIds
One audience at a time. It works only on active automations, and the contacts
enter from the entry, so the steps that send do send: ask for confirmation as
you would for activate_automation. Anyone already inside is ignored (one
enrollment per contact) and templates using {{event_*}} find empty values, because
no event fired here.
The operation is asynchronous: the response says it has been queued. For the outcome
use get_automation_log.
Events
wait_for_event nodes and custom entries are fed by the events API:
POST https://sending.dev/api/v1/events
{ "name": "cart.abandoned", "email": "[email protected]", "properties": { ... } }An event can bring new contacts into the flow and release the ones waiting on a
wait_for_event.
Working out what happened
When a contact "got nothing", do not guess:
get_automation_log { automationId, email: "[email protected]" }
It lists the steps that were walked, with their outcome. A skipped status is not
an error: it says what stopped that step, and it is almost always the answer
(address on the suppression list, plan quota reached, missing template or resource).
With nodeId you narrow it to a single step of the flow.
For the overall effect on one person:
get_contact_journey { email }
It returns messages, inbound mail, touches and conversions in one timeline, telling transactional apart from marketing.
Attachments in nodes
The message node carries attachmentIds: upload first with upload_attachment
and put the id there. Do not delete those attachments while the automation is
active, or the deferred sends will break.

