Automations API
Automations turn events (records, pages, forms, schedules, webhooks, manual
triggers) into ordered action pipelines. The API owns the
definition surface (CRUD + status machine + trigger/condition/action
editors + manual run + run history); execution lives in the worker
(BullMQ queue automations, factory:events stream consumer, 5-minute
schedule reconciler) — see
../architecture/automation.md.
Shared conventions (auth, error model, pagination): authentication.md.
Permission note: listing/creating requires VIEW/EDIT on the
workspace (or a database-scope grant when the automation is scoped to a
database — assertCan(…, { resource: { databaseId } }, "EDIT")); all
automation-scoped routes re-check against the row's workspace (the
workspace is NEVER taken from the URL).
Single source of truth. Trigger types, the condition expression schema, the 9 action config schemas, cron validation, the status machine and the loop-prevention caps all live in
packages/shared/src/automation.ts. The API validates against these schemas; the worker re-validates stored actions before dispatch (defense in depth).
1. Objects
Automation summary (list rows)
{
"id": "cmu…",
"name": "Triage new tickets",
"description": null,
"databaseId": "cmu…",
"status": "ACTIVE",
"triggers": ["RECORD_CREATED"],
"hasCondition": true,
"actions": ["UPDATE_RECORD", "CREATE_NOTIFICATION"],
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:05:00.000Z",
"createdById": "cmu…",
"latestRun": { "id": "…", "status": "SUCCEEDED", "createdAt": "…", "finishedAt": "…", "retryCount": 0, "error": null }
}
Automation detail (GET /automations/:id, create/update/setStatus
responses): the summary plus the full sub-resources:
{
"triggers": [{ "type": "RECORD_CREATED", "config": { "databaseId": "cmu…" } }],
"condition": { "field": { "ref": "property", "name": "Status" }, "operator": "eq", "value": "B" },
"actions": [{ "type": "UPDATE_RECORD", "config": { "…": "…" }, "retries": 3, "timeoutMs": 30000 }]
}
WEBHOOK triggers never return the raw secret (the response sanitizes it); regenerate it with the route below.
Run summary (run list rows)
{
"id": "cmu…",
"status": "PENDING | RUNNING | SUCCEEDED | FAILED | DEAD_LETTER",
"triggerType": "record.created",
"startedAt": "… | null",
"finishedAt": "… | null",
"retryCount": 0,
"error": null,
"createdAt": "…"
}
triggerType is the event type flattened onto triggerEvent (e.g.
record.created, schedule, manual, the incoming webhook event name).
error (terminal failures only):
{ "code": "ACTION_TIMEOUT", "message": "…", "actionIndex": 1, "actionType": "SEND_WEBHOOK", "attempts": 8 }
Run detail (GET …/runs/:runId): the summary plus input, output and
logs[] ({ level, message, data, createdAt } — every step the pipeline
took: run started, per-action started/done/failed, run completed /
skip reasons; this is what the UI log panel renders).
output on success is { actions: [{ index, type, output, ms, status }], totalMs }; on a condition/skip short-circuit it is exactly
{ "skipped": "conditions_not_met" } or { "skipped": "self_trigger" } /
{ "skipped": "depth_limit" }.
2. Routes (all under /api/v1)
| Method | Route | Notes |
|---|---|---|
GET |
workspaces/:workspaceId/automations?status&cursor&limit |
list (default 50, max 200) |
POST |
workspaces/:workspaceId/automations |
create (201 → detail) |
GET |
automations/:id |
detail + latestRun |
PATCH |
automations/:id |
rename / description / re-scope databaseId |
DELETE |
automations/:id |
soft: → ARCHIVED (runs retained) |
POST |
automations/:id/status |
{ status } — the status machine |
PUT |
automations/:id/triggers |
{ triggers: [{ type, config }] } — replace all (1..3, OR-semantics) |
PUT |
automations/:id/conditions |
`{ condition: <tree> |
PUT |
automations/:id/actions |
{ actions: [{ type, config, retries?, timeoutMs? }] } — replace all (1..50, position-ordered) |
POST |
automations/:id/webhook/regenerate |
new secret (old one invalidated); the webhook URL is derived from it |
POST |
automations/:id/run |
manual run → 202 { runId } (DRAFT allowed; body { input? } capped 64 KB) |
GET |
automations/:id/runs?status&cursor&limit |
run history → { data, nextCursor } |
GET |
automations/:id/runs/:runId |
run detail (summary + input/output/logs) |
POST |
automations/:id/runs/:runId/retry |
re-run a FAILED/DEAD_LETTER run → { runId, status: "PENDING" } |
Create body
{
"name": "Triage new tickets", // 1..100, required
"description": "…", // ≤ 500, optional
"databaseId": "cmu…", // optional scope (must exist, active, same workspace)
"triggers": [{ "type": "RECORD_CREATED", "config": { "databaseId": "cmu…" } }],
"condition": { "field": { "ref": "property", "name": "Status" }, "operator": "eq", "value": "B" },
"actions": [{ "type": "UPDATE_RECORD", "config": { "recordId": "from_trigger", "propertyValues": { "Status": "opt-b" } } }]
}
All sub-resources are optional at create time (a DRAFT without triggers simply matches nothing — there is deliberately no completeness gate on activate; the transition table is the only gate).
Trigger configs (per type)
| Type | Config |
|---|---|
RECORD_CREATED / RECORD_UPDATED / PROPERTY_CHANGED / PAGE_CREATED / FORM_SUBMITTED |
record/form: { databaseId } (PROPERTY_CHANGED: + properties: ["Name", …] — the changed-property filter); PAGE_CREATED: { pageId? } |
SCHEDULE |
{ cron: "* * * * *", timeZone?, runEvery? } — cron validated (5/6-field, minimum 60 s interval → 422 CRON_TOO_FREQUENT; invalid → CRON_INVALID) |
WEBHOOK |
{ secret } — auto-generated when absent; incoming calls go to POST /webhooks/incoming/:secret (see webhooks.md for the HMAC/timestamp scheme) |
Status machine
DRAFT ─→ ACTIVE ─→ PAUSED ─→ ACTIVE
│ │ │
└────┬────┴────────┘
▼
ARCHIVED (DELETE)
POST /automations/:id/status returns 409 on the no-op and on any
non-adjacent pair ({ reason: "invalid_transition", from, to }).
Activating a SCHEDULE automation registers its BullMQ repeatable on the
worker's next reconcile (boot / 5 min) — the API itself never touches the
queue.
Runs: retry semantics
POST …/runs/:runId/retry accepts only FAILED and DEAD_LETTER runs
(409 otherwise, e.g. no_op/not_retryable). It resets retryCount to
0, keeps the run row (same id — the history is append-only), sets status
PENDING and re-enqueues the run-automation job. Worker-managed retries
of failing actions use the backoff schedule min(2^(n-1), 600) s
(attempts 1..AUTO_RUN_MAX_RETRIES, default 8); a run that still fails
on the last attempt is dead-lettered (DEAD_LETTER) and workspace
admins get a notification — see the architecture doc §4.
3. Errors
Standard error envelope ({ error: { code, message, details?, requestId } }):
400/422 for schema violations (the Zod issues are passed through in
details), 403 for permission failures, 404 for unknown ids (or ids in
other workspaces), 409 for invalid status transitions / non-retryable runs,
422 for cron violations (CRON_TOO_FREQUENT / CRON_INVALID) and for
create-with-unknown-databaseId.