Skip to content
Contents
Sending and receiving

Automations

An automation is a rule the platform runs for you: when this happens, and these conditions hold, do that. The definition is DATA — a closed vocabulary of triggers, conditions and actions, validated when you save it — so there is nothing to deploy and nothing arbitrary to execute.

The automation

One trigger, an optional condition tree, and an ordered list of actions. priority decides which rule runs first when several match the same event.

FieldDescription
idThe automation's public id.
trigger_type, triggerWhat starts it, and the trigger's own options.
conditionsA tree that must hold before the actions run.
actionsWhat to do, in order.
priorityLower runs first among rules matching the same event.
enabledDisabled rules match nothing, immediately.
versionBumped on every definition change. A run records the version it ran.
statsrun_count, success_count, failure_count, last_run_at, last_error_at, last_error.
next_run_atFor a schedule trigger only; null otherwise.

Authentication and scopes

There is no read scope

Every route in this section — including the reads — requires automations.manage. A role that can only look at automations does not exist, so do not expect one to work.

Endpoints

GET/v1/automationsPOST/v1/automationsGET/v1/automations/{automationId}PATCH/v1/automations/{automationId}DELETE/v1/automations/{automationId}POST/v1/automations/{automationId}/enablePOST/v1/automations/{automationId}/disablePOST/v1/automations/{automationId}/testGET/v1/automations/{automationId}/runsGET/v1/automations/{automationId}/runs/{runId}

The list accepts enabled and trigger_type filters and is cursor paginated; the run log is paginated too and accepts status.

The definition vocabulary

Everything below is a closed list. Anything outside it is refused on save with AUTOMATION_INVALID_DEFINITION, and a PATCH is validated against the MERGED definition rather than against the patch alone.

Triggers

ValueDescription
message_receivedAn inbound message. Takes match (any, equals, contains, starts_with, regex), value and case_sensitive.
conversation_openedA thread opened. Takes reason (created, reopened) and opened_by (inbound, outbound).
conversation_closedA thread closed.
conversation_assignedA thread assigned to somebody.
contact_createdA contact created, from any source.
contact_taggedA tag added. Takes an optional tag to narrow it.
contact_untaggedA tag removed. Takes an optional tag.
scheduleA clock, rather than an event. next_run_at reports the next one.

Conditions

TypeShapeDescription
fieldfield, operator, value?, key?Compares one of the published fields — contact.*, conversation.*, message.*, channel.* — against a value. contact.attribute takes the attribute name in key.
tagoperator, valueWhether the contact has a tag: has or not_has.
time_windowoperator, timezone, from, to, days?A time of day, on named days, in a named timezone: within or outside.
business_hoursoperatorWithin or outside the workspace's business hours, evaluated server-side.
groupoperator, conditions[]Nests conditions under and or or. Groups may contain groups.

A field condition takes one of equals, not_equals, contains, not_contains, starts_with, ends_with, regex, in, not_in, gt, gte, lt, lte, is_set and is_empty, plus an optional case_sensitive.

Actions

TypeShapeDescription
send_messagetextA free-text reply on the conversation.
send_templatetemplate_slug, variables?An approved template, with its variables.
assign_conversationassignee_type, assignee_idAssign the thread to a user membership or to a team.
add_tagtagAdd a tag to the contact.
remove_tagtagRemove a tag from the contact.
close_conversationreason?Close the thread, optionally recording a reason.
reopen_conversationReopen the thread.
add_notebodyAdd an internal note. It is never sent to the contact.
call_webhookurl, method?, headers?Call an HTTP endpoint of yours. GET, POST, PUT or PATCH.
curl
curl -X POST https://whats.azzamkh.sa/api/v1/automations \
  -H "Authorization: Bearer $WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Tag pricing questions",
    "trigger_type": "message_received",
    "trigger": { "match": "contains", "value": "price" },
    "conditions": [
      { "type": "business_hours", "operator": "within" }
    ],
    "actions": [
      { "type": "add_tag", "tag": "pricing" },
      { "type": "assign_conversation",
        "assignee_type": "team", "assignee_id": "tm_..." }
    ],
    "priority": 10
  }'

# 201 Created
# {
#   "id": "atm_...",
#   "enabled": true,
#   "priority": 10,
#   "version": 1,
#   "stats": {
#     "run_count": 0, "success_count": 0, "failure_count": 0,
#     "last_run_at": null, "last_error_at": null, "last_error": null
#   },
#   "next_run_at": null,
#   ...
# }

Dry-running a rule

POST …/test evaluates the REAL conditions against a real subject and executes nothing. The preview and the engine share one evaluator, so a rule that would not run here will not run in production either. It answers trigger_matched, conditions_matched, would_run and a per-action verdict, and is limited to 60 requests a minute per workspace.

curl
curl -X POST https://whats.azzamkh.sa/api/v1/automations/atm_.../test \
  -H "Authorization: Bearer $WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "conversation_id": "cnv_...", "message_text": "what is the price" }'

# 200 OK
# {
#   "automation_id": "atm_...",
#   "trigger_matched": true,
#   "conditions_matched": false,
#   "would_run": false,
#   "reason": "condition_not_met",
#   "subject": {
#     "conversation_id": "cnv_...", "contact_id": "cnt_...",
#     "message_id": null, "channel_id": "ch_..."
#   },
#   "actions": [
#     { "index": 0, "type": "add_tag", "would": "skip",
#       "reason": "conditions_not_met" }
#   ],
#   "evaluated_at": "2026-08-14T09:00:00.000Z"
# }

The run log

Every execution is recorded, whether it did anything or not. A run carries the automation_version it ran, the subject it ran against, an outcome per action, and duration_ms.

StateDescription
pendingClaimed and not started.
runningIn progress.
completedEvery action succeeded.
partialSome actions succeeded and some did not.
failedThe run failed. error carries the reason.
skippedThe trigger matched but the conditions did not.
blockedRefused by a loop guard or an execution limit.
  • chain_depth and causation_run_id exist because an action can trigger another rule. The engine bounds the chain rather than letting two rules answer each other forever.
  • Each entry in actions reports status (succeeded, skipped, failed) with a reason, so a partial run tells you exactly which step gave up.
  • A run is credited to the definition version it executed, so editing a rule does not rewrite the history of what already ran.

Errors that matter here

CodeHTTPWhen it happens
AUTOMATION_NOT_FOUND404No automation with that id in this workspace.
AUTOMATION_RUN_NOT_FOUND404No such run on that automation.
AUTOMATION_INVALID_DEFINITION422The definition uses something outside the vocabulary, or contradicts itself.
AUTOMATION_LIMIT_REACHED409An execution limit or loop guard stopped it.
ENTITLEMENT_LIMIT_REACHED409The plan does not include automations, or a limit is reached.

The welcome and away replies are automations too — see business hours and auto-replies. Every code and its status is on the errors page.