DM Champ Docs

Automations API

An automation is a workflow made of steps: one or more triggers that start it, then actions, conditions, delays, AI steps and the rest, connected into a graph. It is the same automation you see and edit on the Automations page. This guide covers reading and building them over the API.

All examples below show the ?apiKey= query form in cURL and the X-API-Key header in JavaScript and Python. Either works on every endpoint.

Agencies: every endpoint on this page accepts sub_account_id as a query parameter, so you can read and build a client’s automations with your agency key. A sub_account_id that is not one of your client accounts returns 404. See API for Agencies.


How it works: draft first

The API is built so that nothing starts running by accident.

  1. Read the catalogue. GET /automations/steps lists every trigger, action and step kind you can use, plus the grammar for writing a graph (graph_rules).
  2. Create a draft. POST /automations stores the automation switched off. Nothing runs yet.
  3. Check it. Read it back, open it in the builder (the response gives you its open_path), and make sure the person who owns the account has seen what it does and to whom.
  4. Switch it on. POST /automations/{automationId}/enable is its own call. This is the step that makes real things happen: messages sent, records changed, tools given to an AI agent.

Editing never changes whether an automation is on or off. That also means an edit to an automation that is on takes effect right away, on the live automation. To change a running automation safely, disable it, edit it, check it, and enable it again.

Every graph you send goes through the same checks the builder canvas applies. A graph it cannot open or run is refused with a 400 that lists exactly what to fix.

Access

  • The account needs Automations. Without it, every call on this page returns 403.
  • A team member’s access follows their Automations area in Team Management: reading (list, get, steps, runs, folders) needs View, everything that changes something needs Edit.
  • On a client account whose plan leaves out creating and editing automations, the write calls return 403, exactly as the builder does in the app.

The automation object

The list returns a summary of each automation, without its steps:

{
  "id": "8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f",
  "name": "Price question auto-reply",
  "description": "Replies with the price list when someone asks about prices.",
  "enabled": false,
  "updated_at": "2026-10-27T09:40:00.000Z",
  "trigger_kind": "incoming_message",
  "folder_id": "f1a2b3c4-5d6e-7f80-91a2-b3c4d5e6f708",
  "folder_name": "Sales"
}
Field Meaning
id The automation’s id.
name What it is called in the list.
description One to three sentences on what it does, or null.
enabled Whether it is switched on. A disabled automation never runs.
updated_at When it was last changed.
trigger_kind What sets it off, for example incoming_message, schedule, webhook, agent_function or manual. null when the graph has no trigger yet.
folder_id, folder_name The folder it is filed in, or null for no folder.

Reading one automation adds created_at and its graph:

{
  "id": "8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f",
  "name": "Price question auto-reply",
  "description": "Replies with the price list when someone asks about prices.",
  "enabled": false,
  "created_at": "2026-10-27T09:12:00.000Z",
  "updated_at": "2026-10-27T09:40:00.000Z",
  "trigger_kind": "incoming_message",
  "folder_id": null,
  "folder_name": null,
  "graph": {
    "nodes": [
      {
        "id": "trigger_1",
        "kind": "trigger",
        "label": "Incoming message",
        "alias": "incoming_message",
        "action": "incoming_message",
        "params": { "message_filter_contains": "price" },
        "config": { "kind": "incoming_message", "message_filter_contains": "price" }
      },
      {
        "id": "send_prices",
        "kind": "action",
        "label": "Send message",
        "alias": "send_message",
        "action": "SEND_MESSAGE",
        "params": { "contact_id": "${trigger.contact.id}", "body": "Hi ${trigger.contact.first_name}, here is our price list: https://example.com/prices" },
        "config": { "provider": "native", "slug": "SEND_MESSAGE", "params": { "contact_id": "${trigger.contact.id}", "body": "Hi ${trigger.contact.first_name}, here is our price list: https://example.com/prices" } }
      }
    ],
    "edges": [{ "from": "trigger_1", "to": "send_prices" }]
  }
}

Each step (nodes[]) carries:

Field Meaning
id The step’s id, unique within the graph.
kind The kind of step: trigger, action, condition, transform, ai, delay, loop or respond. See Step kinds.
label The name shown on the step in the builder.
alias The step’s reference name. Later steps read its output as ${nodes.<alias>.<field>}. null on a step that has none (it is then referenced by its id).
action What the step does: the action it runs, or on a trigger step the trigger kind. null on other steps.
params The action’s parameters on an action step, or the trigger’s settings on a trigger step. null on every other kind. Kept for older readers: config carries the same values and more.
config The step’s full settings as the builder stores them, for every kind of step: a condition’s rules, an AI step’s prompt, a delay’s duration, an action’s slug and parameters.

Each connection (edges[]) has from and to (step ids), and branch when it follows one branch of a step, for example true or false after a condition.

Two things in config are changed on the way out:

  • Secrets are hidden. Values that look like credentials (webhook secrets, tokens, API keys, passwords, header values) come back as "[redacted]". Send "[redacted]" back unchanged and the stored value is kept.
  • Long texts are shortened. A single text longer than about 4,000 characters is cut short and ends with an ellipsis. If you send that step back, put its full text back in first.

List automations

GET /automations returns the automations on the account, most recently changed first, as summaries (no steps).

Query parameters (all optional): folder_id (only the automations in that folder, an id from List folders, or none for the ones in no folder), sub_account_id.

cURL

curl "https://api.dmchamp.com/v1/automations?apiKey=YOUR_API_KEY&folder_id=none"

JavaScript

const res = await fetch("https://api.dmchamp.com/v1/automations?folder_id=none", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { automations } = await res.json();

Python

res = requests.get(
    "https://api.dmchamp.com/v1/automations",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"folder_id": "none"},
)
automations = res.json()["automations"]

Response (200)

{ "success": true, "automations": [{ "id": "8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f", "name": "Price question auto-reply", "enabled": false, "...": "..." }] }

If the automation service cannot be reached you get a 503, never an empty list in its place. An empty list always means there are no automations.


Get an automation

GET /automations/{automationId} returns one automation with its full graph: every step’s kind, label, alias, action, parameters and settings, plus the connections between them.

curl "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f?apiKey=YOUR_API_KEY"
const res = await fetch("https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { automation } = await res.json();
res = requests.get(
    "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
automation = res.json()["automation"]

Response (200)

{ "success": true, "automation": { "id": "8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f", "name": "Price question auto-reply", "graph": { "nodes": ["..."], "edges": ["..."] } } }

An id that is not on this account returns 404.


Step catalogue

GET /automations/steps returns everything an automation can be built from:

  • node_kinds: the 8 kinds of step, each with a label and a description.
  • triggers: every trigger that can start an automation, with its settings fields (fields) and the values it hands to later steps (output_keys).
  • actions: the built-in actions an action step can run, each with its key (the slug you use in a graph) and its params, marked required where they are.
  • api_actions: the operations of this API that an action step can call. Without q they come back as labels grouped by area in api_actions_by_tag, and the flat list is empty, because there are several hundred of them.
  • graph_rules: the grammar for writing a graph: the data each step kind carries, every trigger kind with its settings keys, the action providers, the branch names and the template syntax. Read it before you create or edit one.
  • counts: the totals per list, whatever you searched for.

This is what the builder offers, not what the account has built. Check it before you conclude that a trigger or action does not exist.

Query parameters (all optional): q, a word to search every list for (case-insensitive), and sub_account_id.

With q:

  • api_actions is the flat list of matches, capped at 60. api_actions_truncated is true when the cap applied. The first matches carry their params.
  • Actions for connected third-party apps are searched too and come back in app_actions. Search by the app’s name, for example q=hubspot. Each one says whether the account has that app connected (connected), and connected_apps lists the apps the account has connected.
  • app_actions_unavailable is true when the app actions could not be searched this time. The other lists are still complete.
curl "https://api.dmchamp.com/v1/automations/steps?apiKey=YOUR_API_KEY&q=tag"
const res = await fetch("https://api.dmchamp.com/v1/automations/steps?q=tag", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { triggers, actions, graph_rules } = await res.json();
res = requests.get(
    "https://api.dmchamp.com/v1/automations/steps",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"q": "tag"},
)
catalogue = res.json()

Response (200, shortened)

{
  "success": true,
  "q": "tag",
  "node_kinds": [{ "kind": "trigger", "label": "Trigger", "description": "Starts the automation when an event happens." }],
  "triggers": [
    {
      "kind": "tag_applied",
      "label": "Tag applied",
      "group": "Contacts",
      "fields": ["tag_name", "channel", "applied_by"],
      "output_keys": ["contact.id", "contact.first_name", "..."]
    }
  ],
  "actions": [
    {
      "key": "SET_CONTACT_TAG",
      "label": "Set contact tag",
      "group": "Contacts",
      "provider": "native",
      "params": [{ "key": "contact_id", "required": true }, { "key": "tag", "required": true }]
    }
  ],
  "api_actions": ["..."],
  "app_actions": [],
  "graph_rules": "GRAPH: { nodes: [{ id, type, label?, data }], edges: [{ source, target, sourceHandle? }] } ...",
  "counts": { "node_kinds": 8, "triggers": 40, "actions": 44, "api_actions": 454 }
}

The lists of step kinds and triggers below are taken from this endpoint. The endpoint itself is always the current list.


Create an automation

POST /automations creates an automation from a name and a graph. It is stored switched off, as a draft.

Body

Field Required Meaning
name Yes What it is called in the list. Short and specific.
description No One to three sentences on what it does and when it runs.
graph Yes { nodes, edges }, see below.
folder_id No The folder to file it in, an id from List folders. Left out or null, it goes in no folder.

The graph. graph_rules from the step catalogue is the full grammar. In short:

  • Each node has an id (unique in the graph, for example trigger_1 or send_reminder), a type (one of the 8 step kinds), an optional label, and its settings in data, under a key named after its type: data.trigger = { kind, ... }, data.action = { provider, slug, params }, data.condition = { ... }, data.delay = { seconds }, data.ai = { prompt }, data.loop = { over }, data.transform = { language, code }, data.respond = { status, body, content_type }.
  • An automation needs at least one trigger. The live grammar allows up to 10, and the automation starts when any of them fires.
  • Every other step must be reachable from a trigger, and no connection may lead back to an earlier step.
  • Each edge has a source and a target. Edges leaving a condition carry sourceHandle true or false. Edges leaving a loop carry body or done. Edges leaving a Wait for reply delay carry replied or timed_out. Other edges have no sourceHandle; the grammar lists the rest.
  • Action slugs and params come from the catalogue. Never guess an id (an agent, a tag, a list, a stage): look it up with the API first, or leave the field as "" for the person to pick in the builder.
  • Positions are optional. Steps without one are laid out automatically.
  • Values from earlier steps are written as ${trigger.<field>} and ${nodes.<step>.<field>}. See Using what an earlier step returned and Expressions in the Automations guide.

The graph is accepted in two spellings: the builder’s own (nodes with type and data, edges with source and target, as below) and the one Get an automation answers with (nodes with kind, config, action, params; edges with from, to, branch). So you can read an automation, change a step and send it back.

curl -X POST "https://api.dmchamp.com/v1/automations?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Price question auto-reply",
    "description": "Replies with the price list when someone asks about prices.",
    "graph": {
      "nodes": [
        { "id": "trigger_1", "type": "trigger", "label": "Incoming message",
          "data": { "trigger": { "kind": "incoming_message", "message_filter_contains": "price" } } },
        { "id": "send_prices", "type": "action", "label": "Send message",
          "data": { "action": { "provider": "native", "slug": "SEND_MESSAGE",
            "params": { "contact_id": "${trigger.contact.id}", "body": "Hi ${trigger.contact.first_name}, here is our price list: https://example.com/prices" } } } }
      ],
      "edges": [{ "source": "trigger_1", "target": "send_prices" }]
    }
  }'
const res = await fetch("https://api.dmchamp.com/v1/automations", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Price question auto-reply", graph: { nodes, edges } }),
});
const { automation, draft, open_path, warnings } = await res.json();
res = requests.post(
    "https://api.dmchamp.com/v1/automations",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"name": "Price question auto-reply", "graph": {"nodes": nodes, "edges": edges}},
)
draft = res.json()

Response (201)

{
  "success": true,
  "automation": { "id": "8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f", "name": "Price question auto-reply", "enabled": false, "graph": { "...": "..." } },
  "draft": true,
  "open_path": "/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f",
  "warnings": []
}
  • automation is the automation as stored, in the same shape as Get an automation.
  • draft is true while it is switched off. A new automation is always a draft until you enable it, so enabled: false here is normal, not a failure.
  • open_path is where it opens in the app’s builder.
  • warnings lists things the builder could not finish, as short labels (for example agent_function:not_linked). It is empty when everything is clean.

A create is not safe to repeat blindly. If a request times out, list your automations before you send it again, or you may end up with two drafts.


Update an automation

PUT /automations/{automationId} changes the name, the description and/or the graph. Send only the fields you want to change.

Body: name, description, graph (all optional, at least one).

A graph replaces the whole graph, so always work from what is stored:

  1. Read the automation with GET /automations/{automationId}.
  2. Change what you need in that graph.
  3. Send the whole graph back, every step included.

When you send it back:

  • Keep every step’s alias exactly as you got it. Later steps refer to a step by its alias (${nodes.<alias>.<field>}). Change or drop it and every reference to that step comes out empty. See Naming your steps.
  • Leave "[redacted]" values as they are. A value shown as "[redacted]" keeps its stored value.
  • Restore shortened texts. A text that came back ending in an ellipsis was cut short in the read.

Whether the automation is on is never changed here. If it is on, your edit is live as soon as it is saved. While an automation is on, a change that would make it trigger itself is refused: disable it first.

curl -X PUT "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Price question auto-reply (EN)" }'
// Read, edit, send back the whole graph
const get = await fetch(`https://api.dmchamp.com/v1/automations/${id}`, { headers: { "X-API-Key": "YOUR_API_KEY" } });
const { automation } = await get.json();
const step = automation.graph.nodes.find((n) => n.id === "send_prices");
step.config.params.body = "Hi ${trigger.contact.first_name}, our new price list: https://example.com/prices";
await fetch(`https://api.dmchamp.com/v1/automations/${id}`, {
  method: "PUT",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ graph: automation.graph }),
});
url = f"https://api.dmchamp.com/v1/automations/{automation_id}"
automation = requests.get(url, headers={"X-API-Key": "YOUR_API_KEY"}).json()["automation"]
# ...edit automation["graph"] here, keeping every alias...
requests.put(url, headers={"X-API-Key": "YOUR_API_KEY"}, json={"graph": automation["graph"]})

Response (200): the same shape as a create (automation, draft, open_path, warnings). Sending none of name, description or graph returns 400.


Enable and disable

POST /automations/{automationId}/enable switches an automation on, so it runs on its trigger from now on. Call it only once the person who owns the account has seen what the automation does and to whom, and has agreed to switch it on. The builder refuses to switch on a graph it cannot run (no trigger, a loop, a step that triggers itself) with a 400 that says why.

POST /automations/{automationId}/disable switches it off (pauses it). Runs already in progress finish; no new run starts. Disabling is always allowed.

Neither call takes a body.

curl -X POST "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/enable?apiKey=YOUR_API_KEY"
await fetch("https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/disable", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
requests.post(
    "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/enable",
    headers={"X-API-Key": "YOUR_API_KEY"},
)

Response (200): the automation as stored now, in the same shape as a create, with draft: false after an enable.


Run history

GET /automations/{automationId}/runs returns the runs of one automation, newest first: when each run started and finished, what set it off, whether it succeeded, and the error when it failed. Reading the automation tells you what it would do. The run history tells you what it did, and it is the only way to tell an automation that never ran from one that runs and fails every time.

Query parameters (all optional): limit (1 to 50, default 10), sub_account_id.

curl "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/runs?apiKey=YOUR_API_KEY&limit=20"
const res = await fetch("https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/runs?limit=20", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { runs, note } = await res.json();
res = requests.get(
    "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/runs",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"limit": 20},
)
runs = res.json()["runs"]

Response (200)

{
  "success": true,
  "automation": { "id": "8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f", "name": "Price question auto-reply", "enabled": true, "trigger_kind": "incoming_message" },
  "runs": [
    {
      "id": "run_7f3a",
      "status": "failed",
      "trigger_source": "incoming_message",
      "started_at": "2026-10-27T10:02:11.000Z",
      "finished_at": "2026-10-27T10:02:12.000Z",
      "error": "Contact is on Do Not Disturb list",
      "cancel_reason": null,
      "mode": "live"
    }
  ]
}
  • mode is live for a real run, or safe for a Safe run from the editor. A Safe run sends nothing, so it is no proof the automation works in real use.
  • An empty runs list is an answer, not a gap. The response then carries a note: either the trigger has never fired, or the automation is switched off, so nothing has run.

Folders

Folders only group automations in the list. They never change what an automation does or whether it is on. See Organizing automations in folders.

List folders

GET /automations/folders returns the folders, each with how many automations it holds, plus unfiled_count: how many automations are in no folder.

curl "https://api.dmchamp.com/v1/automations/folders?apiKey=YOUR_API_KEY"
{
  "success": true,
  "folders": [{ "id": "f1a2b3c4-5d6e-7f80-91a2-b3c4d5e6f708", "name": "Sales", "automation_count": 3 }],
  "unfiled_count": 5
}

Create a folder

POST /automations/folders with { "name": "Appointment reminders" }. The name is required, up to 100 characters, and must be unique on the account. File automations in it with Move an automation, or pass folder_id when you create one.

curl -X POST "https://api.dmchamp.com/v1/automations/folders?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Appointment reminders" }'

Response (201): { "success": true, "folder": { "id": "...", "name": "Appointment reminders", "automation_count": 0 } }. A name another folder already has returns 409 with code: "folder_name_taken".

Rename a folder

PUT /automations/folders/{folderId} with { "name": "..." }. The automations in it stay where they are.

Response (200): { "success": true, "folder": { ... } }.

Delete a folder

DELETE /automations/folders/{folderId} deletes the folder only. The automations in it are not deleted: they move to no folder and keep running exactly as before.

curl -X DELETE "https://api.dmchamp.com/v1/automations/folders/f1a2b3c4-5d6e-7f80-91a2-b3c4d5e6f708?apiKey=YOUR_API_KEY"
{ "success": true, "deleted": true, "moved_to_no_folder": 3, "message": "Folder deleted. 3 automations were moved to No folder; no automation was deleted." }

Move an automation to a folder

PUT /automations/{automationId}/folder with { "folder_id": "<folder id>" }, or { "folder_id": null } to take it out of its folder. folder_id is required. Only where the automation is listed changes.

curl -X PUT "https://api.dmchamp.com/v1/automations/8d2c7f1e-2a3b-4c5d-9e8f-0a1b2c3d4e5f/folder?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "folder_id": "f1a2b3c4-5d6e-7f80-91a2-b3c4d5e6f708" }'

Response (200): { "success": true, "automation_id": "...", "folder_id": "..." }.


Catalogue

Step kinds

Kind What it does
trigger Starts the automation when something happens.
action Runs one step: a built-in action, an API action, a connected app’s action, an HTTP request or an email.
condition Splits the flow on a rule, into a true and a false branch.
transform Runs a short piece of code to reshape data between steps.
ai Asks the AI with a prompt you write. It can return text, separate fields, or pick one of several branches.
delay Waits for a set time, or waits for the contact to reply (with a timeout).
loop Repeats the steps inside it for each item in a list.
respond Returns a response to the webhook call or the AI agent function call that started the run.

Triggers

These are the 40 triggers the catalogue lists today, grouped as the builder groups them. The Settings column holds the trigger’s own settings keys (fields), which narrow when it fires. Every trigger also takes respect_quiet_hours, run_hours, reentry and stop_on_response; see Automation settings.

Messaging

Trigger (kind) Fires when Settings
Incoming message (incoming_message) A contact sends a message on any channel. campaign (limits it to one AI agent), channel, message_filter_contains (keyword), message_filter_regex (pattern)
Missed call (missed_call) Someone calls one of your WhatsApp Web numbers or your own SMS number and nobody picks up. to_number
Message delivered (message_delivered) A message you sent is confirmed delivered by the channel. channel, agent
Message failed (message_failed) A message you sent could not be delivered, with the error. channel, agent
Message sent (message_sent) Any message leaves your account: the AI’s, yours, a broadcast, a follow-up. Filter it, or it runs on all of them. channel, sent_by
Follow-up sent (follow_up_sent) An automatic follow-up goes out. none
Follow-up skipped (follow_up_skipped) A follow-up was held back because its send condition failed. none
Follow-ups finished (follow_up_exhausted) A contact got the last follow-up in the sequence and nothing more is queued. none
Conversation closed (conversation_closed) A chat is closed, with how it ended and a summary. channel, agent
Broadcast finished (broadcast_completed) A broadcast has finished sending, with how many went out and how many failed. none
Lead reply sorted (outreach_reply_labelled) A cold email lead’s reply is sorted, for example as Interested. label
Skool comment received (skool_comment_received) A member comments under a Skool post one of your comment rules watches. none

Contacts

Trigger (kind) Fires when Settings
New contact (contact_created) A new person reaches you for the first time on any channel, and also for imports, the API and contacts you create by hand. none
Contact updated (contact_updated) A contact field changes. field
Contacts you add here (contact_added) Only for contacts you put into this automation yourself from the Contacts page. One run per contact. none
Tag applied (tag_applied) A tag is added to a contact, by your AI agent, a person, an automation or an import. tag_name, channel, applied_by
Tag removed (tag_removed) A tag is taken off a contact. tag_name, channel
Skool member joined (skool_member_joined) Someone joins a Skool community you own or run, with their profile, answers and fit score. group_id, create_contact
Skool member qualified (skool_member_qualified) A new Skool member’s answers score at or above your threshold. min_score

Deals

Trigger (kind) Fires when Settings
Deal created (deal_created) A new deal is added to one of your pipelines. pipeline_id, stage_id
Deal stage changed (deal_stage_changed) A deal moves to a different stage, naming the stage it left and the one it entered. pipeline_id, to_stage_id, from_stage_id
Deal updated (deal_updated) A deal field changes. field

Appointments

Trigger (kind) Fires when Settings
Appointment booked (appointment_booked) A new appointment is booked. none
Appointment cancelled (appointment_cancelled) A booked appointment is cancelled. none
Appointment rescheduled (appointment_rescheduled) A booked appointment moves to another time, with the old time alongside the new one. event_id
Before an appointment (appointment_upcoming) A set time before each confirmed appointment, for reminders. Up to five reminders per automation. offsets_minutes

Tasks

Trigger (kind) Fires when Settings
Task created (task_created) A task is created. task_type
Task completed (task_completed) A task is moved into a completed stage. none

Account

Trigger (kind) Fires when Settings
Low credit balance (low_credit_balance) Your credit balance drops below your alert threshold. none
Channel connected (channel_connected) One of your channels finishes connecting. channel
Channel disconnected (channel_disconnected) One of your channels drops: logged out, unlinked or cut off. A WhatsApp Web number that reconnects by itself only counts if it is still down after 30 minutes. channel

Agency

Trigger (kind) Fires when Settings
Sub-account created (sub_account_created) A new client account is created, from your dashboard, the API or your signup page. none
Sub-account activity (sub_account_event) Something happens on one of your client accounts: credits running low, a channel connecting, a plan change. event, threshold

General and apps

Trigger (kind) Fires when Settings
On schedule (schedule) On a cron schedule. cron, timezone
Webhook (webhook) An outside system calls the automation’s webhook address. webhook_secret
AI Agent Function (agent_function) Your AI agent calls this automation as a function during a conversation. function_name, function_description, parameters, agent_ids, campaign_ids
New feed item (rss) A new item appears in an RSS or Atom feed. feed_url
Email received (email_received) An email arrives in your own connected mailbox. from_contains, subject_contains
App event (app_event) Something happens in a connected app: a deal stage changes, a payment comes in, a form is submitted. toolkit, trigger_slug, connected_account_id, config
Manual run (manual) Only when it is started by hand. none

An automation can hold at most one trigger each of webhook, app_event, agent_function, rss, schedule, email_received, appointment_upcoming and contact_added. A trigger that fires when a record is created can’t be paired with the action that creates the same record, because the automation would set itself off.

For what each trigger hands to later steps (output_keys) and the full list of actions with their params, read GET /automations/steps. The Automations guide explains each trigger in the builder’s words.


Errors

Every error has the usual shape { "success": false, "error_code": <status>, "error": "..." }. A refused graph adds code and errors:

{
  "success": false,
  "error_code": 400,
  "code": "invalid_graph",
  "error": "<the first problem, as one sentence>",
  "errors": ["<the first problem, as one sentence>", "<the next problem>"]
}

errors lists every problem, one fixable sentence each, and error is the first one. Fix exactly what is listed and send the whole graph again. code is one of invalid_graph, automation_graph_has_a_cycle, self_triggering_automation, no_trigger, or another code from the builder.

Status When
400 The graph was refused (see above), a required field is missing (name or graph on a create, folder_id on a move), an update sent nothing to change, an automation or folder id is not valid, or a folder name is missing, empty or too long (code: "invalid_name").
403 The account does not have Automations; a client account’s plan does not include creating and editing automations (writes only); or the team member’s Automations access is too low (View for reads, Edit for writes).
404 No automation with that id on this account, no folder with that id (code: "folder_not_found"), or a sub_account_id that is not one of your client accounts.
409 A folder with that name already exists (code: "folder_name_taken").
413 The automation is larger than the 1 MB the builder accepts. Move large code or data out of its steps.
422 The builder rejected the request for another reason. Check the graph against GET /automations/steps; sending the same request again will not help.
503 The automation service could not be reached. Try again in a moment. For a create, list your automations first so you don’t make a second draft.

Using these endpoints over MCP

Every endpoint on this page is also a tool for an AI assistant you connect over MCP, with the same rules: a new automation is a draft until it is enabled, and an edit to a running automation is live.

Tool Endpoint
list_automations GET /automations
get_automation GET /automations/{automationId}
list_automation_steps GET /automations/steps
create_automation POST /automations
update_automation PUT /automations/{automationId}
enable_automation POST /automations/{automationId}/enable
disable_automation POST /automations/{automationId}/disable
list_automation_runs GET /automations/{automationId}/runs
list_automation_folders GET /automations/folders
create_automation_folder POST /automations/folders
rename_automation_folder PUT /automations/folders/{folderId}
delete_automation_folder DELETE /automations/folders/{folderId}
move_automation_to_folder PUT /automations/{automationId}/folder

Next steps

  • Automations: how triggers, steps, variables and the run history work in the builder.
  • API for Agencies: build automations on a client account with sub_account_id.
  • API Reference: the full interactive endpoint explorer.