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
  1. Create. POST /v1/automations takes your logic and returns a draft. A draft never runs and never counts against the number of automations your plan allows.
  2. Version. POST /v1/automations/{id}/versions adds a new version. Versions are append-only and numbered per automation; this call validates and never deploys.
  3. Validate. POST /v1/automations/{id}/validate reports what is wrong without changing anything. It is cheap enough to call on every edit.
  4. Deploy. POST /v1/automations/{id}/deploy compiles the version and stores the compiled artefact against it. Deploying does not make it live.
  5. Enable. POST /v1/automations/{id}/enable points 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:

  1. Platform default — off. There are no quiet hours unless somebody sets them.
  2. Organization-wide, in settings.
  3. 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_actions on 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-disabledstatus becomes auto_disabled and auto_disabled_reason says 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_storm rather 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_limit appears 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.