Write an automation as steps
The dashboard builds automations as a canvas of connected bricks. That canvas is stored as a node graph, and for a long time the only way to make one programmatically was to hand-assemble it — inventing internal ids, wiring edges between them, numbering branch arms, and supplying canvas coordinates that the builder recomputes anyway.
A step document is the same automation written as a list of steps. It is what POST /api/automations and PUT /api/automations/:id/graph accept as document, what the CLI's rule:create sends, and what an AI assistant connected over MCP writes.
The shape
{
"format": "big.automation/v1",
"name": "Follow first → DM your link",
"trigger": { "type": "dm", "keywords": ["VIP"] },
"steps": [
{ "id": "ask", "dm": "Follow me and I'll send the link over. Reply once you have." },
{ "id": "check", "branch": {
"max_attempts": 2,
"arms": [
{ "when": "is_follower", "goto": "link" },
{ "when": "otherwise", "goto": "nudge" },
{ "when": "loop_exhausted", "goto": "letgo" }
]
}}
],
"paths": {
"link": [{ "dm": "You're in. Here's the link: https://example.com" }],
"nudge": [{ "dm": "I can't see the follow yet — reply again and I'll look.", "goto": "check" }],
"letgo": [{ "dm": "All good, no hard feelings." }]
}
}That is the whole follower gate: ask, check, send or nudge, and give up gracefully.
Steps and paths
steps is the spine. Each step flows into the next one, so a plain keyword → DM automation is two steps and no wiring at all. The last step ends the conversation.
paths are named blocks, reached only by name — from a branch arm or an explicit goto. Each path runs top to bottom and then stops, unless its last step carries a goto.
A branch ends the spine. Everything after a branch is reached through its arms, never by falling through.
The split between the two is deliberate. With one flat list, a step that sends your link and a step that says "I can't see the follow yet" would be connected just because one was written above the other — and that automation would be accepted as valid. Naming the blocks means the document says where someone goes, rather than implying it from the order you happened to type things in.
Steps
A step does exactly one thing:
| Key | What it does |
|---|---|
dm | Sends a direct message. Add link to attach a URL. |
branch | Waits for their next reply and routes on it. |
And takes these modifiers:
| Key | Where | What it does |
|---|---|---|
id | required in steps | The name other steps aim at. Not used in paths — the path's own name is its entry. |
goto | any step | Go here instead of falling through. |
choices | on a dm | Quick-reply buttons: [{ "label": "✅ Done", "goto": "check" }]. A step with buttons waits for a tap. |
Branch arms
Arms are tried in the order you write them:
when | Matches |
|---|---|
contains_any | Their reply contains any of values |
equals | Their reply is exactly value |
is_follower | They follow your account — checked live, right then |
loop_exhausted | The re-check ran out of tries |
otherwise | Everything else — every branch needs exactly one |
max_attempts on a branch makes it a bounded loop: an arm can point back at the branch to re-check, and loop_exhausted is where someone lands when the tries run out. See Branch on replies and followers for what that feels like to the person on the other end.
When something is wrong
You get back the message the dashboard would show, naming your step:
branch node check closes a loop but its "after N tries" (loop_exhausted) edge doesn't leave itThe step names you choose are the names in the errors, so there is nothing to translate.
How many steps one can have
max_actions_per_run is optional and defaults to 20. It is also the node ceiling — an automation may not have more steps than the actions one run is allowed — and a document may not set it above 200.
Reading one back
GET /api/automations/:id returns the automation's document alongside its raw graph, so you can read one, change a message, and send it back.
Some automations can't be written as steps. Cards, carousels and media attachments have shapes the format doesn't cover, and a comment-triggered automation has no document at all — whatever its replies look like — because its trigger needs the ids of your own Instagram posts. Those come back with document: null, exact: false, and a documentReason saying which one it was. The graph is always there either way.
What it deliberately leaves out
Anything specific to one workspace: which Instagram account the automation runs on, which posts a comment trigger watches, which tags it applies. A document is meant to be saved in a file, shared, and run in someone else's workspace — so those stay as separate fields on the request.
Comment triggers, for the same reason: they need the ids of your own Instagram posts. Build those in the dashboard, or send a raw graph.
Next step
The REST API reference has the endpoints, and the CLI reference has rule:create, rule:graph and rule:view.