Automations
Creating an automation, versioning it, deploying and enabling it, and reading the runs it produces — the lifecycle, the trigger catalogue, what counts as a run, and the rails that stop a runaway one.
An automation is a piece of logic that starts on something happening — a WhatsApp message arriving,
a lead coming in, a schedule firing — and produces a run. This page is the /v1 surface for
creating, versioning, deploying and watching them.
The automation page covers what automations are for — the shapes people build and why. This page is the API: the lifecycle, the objects and the guarantees.
The lifecycle
An automation is never live by accident. Five steps, and each is its own call, because each can fail for its own reason.
create ──▶ version ──▶ validate ──▶ deploy ──▶ enable ──▶ runs
draft append- report compile activate
only without and store a version
deploying the artefact
- Create.
POST /v1/automationstakes your logic and returns a draft. A draft never runs and never counts against the number of automations your plan allows. - Version.
POST /v1/automations/{id}/versionsadds a new version. Versions are append-only and numbered per automation; this call validates and never deploys. - Validate.
POST /v1/automations/{id}/validatereports what is wrong without changing anything. It is cheap enough to call on every edit. - Deploy.
POST /v1/automations/{id}/deploycompiles the version and stores the compiled artefact against it. Deploying does not make it live. - Enable.
POST /v1/automations/{id}/enablepoints the automation at a version and starts it. For event triggers this is instant and atomic — the dispatcher reads the active version on every event, so there is no propagation delay and no window where half your traffic is on each version.
PATCH /v1/automations/{id} changes the name and description only. Logic is versioned, so
anything that changes what the automation does goes through /versions.
Your logic is opaque on the wire, deliberately
The ir field on POST /v1/automations and POST .../versions carries the automation document.
Its internal grammar is not part of the published contract, and that is a deliberate boundary
rather than an omission: the engine compiles it, and pinning its shape publicly would freeze the
compiler. ir_version selects the schema the server validates against, and a document that does
not match is refused rather than guessed at.
What is contractual is everything on this page: the lifecycle, the trigger kinds, the objects, the run semantics and the failure modes.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET |
/v1/automations |
The organization’s automations |
POST |
/v1/automations |
Create one, as a draft |
GET |
/v1/automations/{id} |
One automation |
PATCH |
/v1/automations/{id} |
Name and description only |
GET |
/v1/automations/{id}/versions |
Every version, newest first. Append-only |
POST |
/v1/automations/{id}/versions |
A new version. Validated, never deployed |
POST |
/v1/automations/{id}/validate |
Report problems without deploying |
POST |
/v1/automations/{id}/deploy |
Compile and store the artefact on the version |
POST |
/v1/automations/{id}/enable |
Activate a version |
POST |
/v1/automations/{id}/disable |
Stop it. Event triggers stop instantly |
POST |
/v1/automations/{id}/rollback |
Point back at an earlier version |
POST |
/v1/automations/{id}/test |
Run it once, now, and report it as a run |
GET |
/v1/runs |
Runs, filterable by automation_id and status |
POST |
/v1/runs |
Trigger an automation by hand |
GET |
/v1/runs/{id} |
One run |
GET |
/v1/runs/{id}/events |
The per-step trace behind a run’s timeline |
POST |
/v1/runs/{id}/cancel |
Cancel a run that has not finished |
POST /v1/automations/{id}/versions accepts an Idempotency-Key.
What starts an automation
One trigger per automation, chosen when you create it and reported as trigger_kind.
trigger_kind |
Fires when |
|---|---|
whatsapp.message_received |
An inbound WhatsApp message arrives |
whatsapp.message_status |
A sent / delivered / read / failed status callback arrives |
whatsapp.session_expiring |
The 24-hour customer service window is closing, N minutes out |
contact.created · contact.updated · contact.tagged |
A contact changes |
lead.created |
A lead arrives from a form, a webhook, a Sheet or Meta Lead Ads |
appointment.upcoming |
A set offset before an appointment — -24h, -2h |
schedule.cron |
A schedule fires. Six-field cron, IANA timezone |
webhook.inbound |
A signed webhook arrives at your automation’s own URL |
manual |
Somebody pressed Run, or called POST /v1/runs |
continuation |
Internal. The engine resuming a run after a long wait — never chosen by you, and it can appear on a run’s trigger.kind |
Duplicate triggers are collapsed for 36 hours. Meta can retry an inbound message for up to that long, and past it a duplicate is acceptable rather than an incident. Where a trigger’s payload carries no stable key to deduplicate on, there is no deduplication — a check that can never fire is not shipped.
The automation object
{
"id": "aut_01K4ACP11C00000000000000A1",
"object": "automation",
"slug": "ai-receptionist",
"name": "AI receptionist",
"description": null,
"status": "enabled",
"trigger_kind": "whatsapp.message_received",
"trigger_config": {},
"connection_id": "conn_01K3F8QY7ZC4M2N9V6XJ0RTBHE",
"active_version_id": "av_01K4ACP11C00000000000000V3",
"consecutive_failures": 0,
"auto_disable_after_failures": null,
"last_run_at": "2026-09-11T09:14:02Z",
"last_error": null,
"auto_disabled_at": null,
"auto_disabled_reason": null
}
status |
Meaning |
|---|---|
draft |
Created, never enabled. Does not run and does not count against your plan’s automation allowance |
enabled |
Live on active_version_id |
disabled |
Stopped by a person |
auto_disabled |
Stopped by us. auto_disabled_reason says why |
archived |
Retired |
connection_id is optional: an automation without one is not bound to a single WhatsApp number.
consecutive_failures is the current streak. auto_disable_after_failures is this automation’s own
threshold where one is set, and null where it follows the default behaviour below.
Versions and rollback
automation_versions is append-only and version_no is a monotone integer per automation. History
is never rewritten.
Rollback is a pointer move, not a rewrite. The previous version’s artefact is still deployed, so
POST /v1/automations/{id}/rollback is one transaction and carries no compile risk. Runs already
in flight on the newer version finish on it — they were dispatched against it, and that is the
correct outcome rather than an oversight.
The last ten deployed versions stay live; older artefacts are archived nightly and removed after 90 days. That figure is the same for every organization and is unrelated to how long your messages, runs and logs are kept, which your plan configures. The automation document itself is kept indefinitely, so a version older than the artefact window is recompiled and redeployed as a new version rather than being lost.
Runs
{
"id": "run_01K4ACP11C00000000000000R2",
"object": "run",
"automation_id": "aut_01K4ACP11C00000000000000A1",
"version_no": 3,
"status": "success",
"skip_reason": null,
"trigger": { "kind": "whatsapp.message_received" },
"started_at": "2026-09-11T09:14:02Z",
"finished_at": "2026-09-11T09:14:04Z",
"duration_ms": 2412,
"usage": { "run_units": 1, "ai_conversation_id": null },
"result": null
}
status |
Meaning |
|---|---|
queued |
Admitted, waiting for a slot |
running |
Executing |
waiting |
Paused — a wait, or a human takeover |
success · failure · cancelled · timeout |
Terminal |
skipped |
Never executed. skip_reason says why |
unknown |
We could not determine the outcome |
A run has a hard ceiling on how long it may take; past it the run ends as timeout rather than
hanging.
Why a run was skipped
skip_reason is set only on skipped, and it is the honest answer to “why did nothing happen”.
quota_exceeded · ai_capped · bot_disabled · outside_service_window · opted_out ·
connection_not_active · concurrency_limit · duplicate · quiet_hours · trigger_storm ·
self_trigger
The timeline
GET /v1/runs/{id}/events returns the per-step trace, in seq order, with step_action, status
and duration_ms on each entry. Every line is generated by us — none of it is raw output from
the execution engine — and error messages are mapped to plain language rather than passed through.
What counts as a run
Your plan includes an allowance of runs. The word has exactly one meaning:
A run is one execution of one automation, from trigger to completion.
Steps inside it do not count. Loop iterations do not count. Internal continuation segments after a long wait do not count. Retries of a failed step do not count. A run skipped by its own guard condition does not count.
Two consequences worth stating, because both cut in your favour:
- A sweep that fires once and sends four hundred reminders is one run. Reminders are messages, and Meta bills your own WhatsApp Business Account for those directly.
- A run that fails is counted — we did the work — except where it failed on our infrastructure. Those are credited back.
The counter increments when the run is dispatched, not when it finishes.
Quiet hours
Three levels, and the most specific one that is set wins:
- Platform default — off. There are no quiet hours unless somebody sets them.
- Organization-wide, in settings.
- Per automation, set by whoever creates it.
Absent means inherit, never off. An automation can opt out of an organization-wide window explicitly. The setting is resolved when an action is taken rather than compiled in, so an organization-wide change reaches automations that are already live without redeploying them — and changing an automation’s own override is an ordinary edit, so it is a new version, diffable and reversible like any other.
The rails
Layered, because any one of them alone is insufficient.
- Destructive actions need an explicit confirmation to deploy. Anything that deletes, cancels or
moves money is named in
confirm_actionson the deploy call, or the deploy is refused. An automation that can reach more than fifty distinct contacts in one run needs the same. - A test run is required before enable where the logic contains a destructive action. It is dispatched exactly like production against a target you nominate, on a strictly limited budget.
- Repeated failure stops it. Three consecutive failures notify you. Above a 50% failure rate
over the last twenty runs, the automation is auto-disabled —
statusbecomesauto_disabledandauto_disabled_reasonsays so — and re-enabling is one call. Re-enabling never hands a conversation back to a bot after a human has taken it over. - A bad new version rolls itself back. If a newly-enabled version fails above 50% in its first twenty runs while the one before it was under 10%, the pointer moves back automatically and you are told. The automation stays enabled, on the older version.
- Trigger storms are shed. Above sixty triggers a minute for one automation, further triggers
are skipped with
trigger_stormrather than queued. This is the defence against a loop in which your own outbound message re-triggers your own inbound handler. - Self-triggering is refused. A run whose trigger was caused by an action more than three runs
up its own ancestry is skipped with
self_trigger. - Concurrency is per organization, at a number your plan configures, so one automation with a
backlog cannot starve the rest of your workspace. Runs above it queue;
concurrency_limitappears as a skip reason only where queueing is not appropriate.
When something is refused
Automation calls use the standard envelope — the error reference has every code. The ones specific to this surface:
| Code | HTTP | When |
|---|---|---|
automation_invalid |
400 | The document did not validate |
automation_not_deployed |
409 | Enable was called on a version with no artefact |
connector_not_connected |
409 | A step needs an integration that is not connected |
connector_needs_reauth |
409 | It is connected, and its authorization has expired |
approval_required |
202 | The run is waiting for a person. Not a failure — treat a 202 here as success |
approval_expired |
409 | Nobody answered in time |
quota_exceeded |
402 | The run allowance is spent |
ai_conversations_capped |
402 | The AI conversation allowance is spent |
concurrency_limit |
429 | Too many runs at once for this organization |
A feature your plan does not include is not an error. It comes back as type: not_entitled with
the feature named and a link to billing, and a client that renders it as a failure has got it wrong.