big docs

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.

You rarely need to start from nothing. Every automation in the template library is a document like this one: big rule:template <id> --document prints one, and GET /api/automation-templates/:id returns it as document.

The trigger

trigger.type is dm, comment or story_reply, and keywords is the list of words that set it off. A dm or story_reply trigger with no keywords answers every message of that kind. A comment trigger needs at least one keyword — there is no catch-all for comments. case_sensitive is optional, and skip_story_replies is for a dm trigger only. Triggers has the matching rules for each.

A comment trigger watches one or more of your posts, and the posts are not in the document. Send them beside it on the request as post_ids, the Instagram media ids that big posts shows as media:<id>. big works out which connected account each post belongs to, so post_connection_ids is optional. A media id big hasn't synced is refused with a message telling you to run big posts:sync first.

{
  "format": "big.automation/v1",
  "name": "Comment a word → DM your link",
  "trigger": { "type": "comment", "keywords": ["LINK"] },
  "steps": [
    { "id": "public-reply", "reply": "Sent it to your DMs 📩" },
    { "id": "send-link",    "dm": "Here's the link you asked for: https://example.com" }
  ]
}

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:

KeyWhat it does
dmSends a direct message. Add link to attach a URL.
branchWaits for their next reply and routes on it.
replyReplies publicly under the comment that fired the automation. Comment triggers only, and it must be steps[0]. Text only: no link, no choices.

And takes these modifiers:

KeyWhereWhat it does
idrequired in stepsThe name other steps aim at. Not used in paths — the path's own name is its entry.
gotoany stepGo here instead of falling through.
choiceson a dmQuick-reply buttons: [{ "label": "✅ Done", "goto": "check" }]. A step with buttons waits for a tap. Not on a comment trigger.

A comment automation sends text only. Its DMs are plain messages, so a choices anywhere in a comment document is refused rather than accepted and silently dropped. Keep it to a reply and a DM or two, sent within Instagram's 7-day comment window.

Branch arms

Arms are tried in the order you write them:

whenMatches
contains_anyTheir reply contains any of values, anywhere in it
equalsTheir reply is exactly value, the whole reply
is_followerThey follow your account — checked live, right then
loop_exhaustedThe re-check ran out of tries
otherwiseEverything else — every branch needs exactly one

contains_any and equals ignore case unless the arm sets "case_sensitive": true, which only those two accept. The dashboard has no toggle for it, so a case-sensitive arm is something you set in a document.

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 it

The 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, and the trigger counts as one node: the steps plus the trigger may not outnumber the actions one run is allowed. At the default of 20, and at the ceiling of 20 (also the most the database stores), an automation holds at most 19 steps. The twentieth is refused with node count (21) exceeds max_actions_per_run (20); raise max_actions_per_run (up to 20) or cut a step. The same 1–20 bound applies to max_actions_per_run on POST and PATCH /api/automations; anything outside it is a 400. Each step's settings may be at most 16 KiB of JSON.

Limits and validation

A document that breaks one of these comes back as a 400 whose message starts big.automation/v1 document is invalid: and names the step ((at steps[1].choices)), or, for the wiring rules, names the step id. These are the exact messages:

RuleMessage
A dm or reply is at most 1000 charactersa dm may be at most 1000 characters
A choice label is at most 20 characters (Instagram's chip limit)a choice label may be at most 20 characters (Instagram's chip cap)
At most 13 choices on one stepat most 13 choices (Instagram's quick-reply cap)
A step with choices can't also have a goto: it waits for a tap. It also ends its block, so the step after it is reached only if a choice names ita step with "choices" may not also have "goto" — the run parks on it awaiting a tap, so it never moves on by itself
A branch routes only through its armsa branch step may not carry "goto" — every route out of a branch is one of its arms (the same for link and choices)
Exactly one otherwise arm per brancha branch needs exactly one "otherwise" arm — it is where every unmatched reply goes
trigger is a reserved step id"trigger" is reserved for the trigger node and may not be used as a step id
Every path is named by somethingpath "nudge" is never reached — no goto, branch arm or choice names it
A loop needs max_attempts on its branch, no larger than max_actions_per_runbranch node check closes a loop and must have a re-check cap (max_attempts)
loop_exhausted goes only on a branch inside a loopbranch node check has an "after N tries" arm but is not part of a loop
Every loop stops at a branch or a step with choicesloop through node nudge has no parking node — every cycle must stop at a branch or a tap-awaiting card
A comment automation sends text onlychoices send nothing on a comment-triggered automation — #dm-engine sends comment automations text only

Matching inside a branch is literal. contains_any matches a value anywhere in the reply, inside a longer word too, so yes also matches "yesterday". equals compares the whole reply, untrimmed, so "yes " with a trailing space doesn't equal yes. Both ignore case unless the arm sets case_sensitive: true.

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.

Comment automations read back too: the public reply comes back as a reply step, and the posts it watches come back on the automation row as post_ids, never inside the document.

Some automations can't be written as steps. Cards, carousels, media attachments and postback buttons have shapes the format doesn't cover, and that includes a comment reply with media attached. Those come back with document: null, exact: false, and a documentReason saying which shape it was. The graph is always there either way.

Build those in the dashboard. On a Send DM step, Message type picks a card, a carousel or quick replies, and Attach a file adds media. The raw node graph that --graph and "graph" accept is the dashboard's own storage format and isn't documented as an authoring format, so don't hand-write cards into one: the one graph worth sending is one you read back (big rule:view <id> --json) and are copying unchanged.

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 adds to the people who fire it, and who it is for. 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: connection_id, post_ids, tag_ids (tags applied on trigger), and the audience lists include_tag_ids and exclude_tag_ids. On the CLI they are --account, --post, --tags, --include-tags and --exclude-tags; big rule:set changes any of them later, along with the keywords, without touching the steps.

That's why a comment document is portable even though a comment automation isn't: the same document runs on whichever post you name when you create it. The remote MCP server can't list your posts, so it refuses comment documents and links you to the dashboard instead. The CLI (--post) and the local MCP server (postIds) can create them.

Next step

The REST API reference has the endpoints, and the CLI reference has rule:templates, rule:template, rule:create, rule:graph, rule:set and rule:view.

Write an automation as steps | itsjustbig