Skip to content

Automations ​

An automation is a graph: one trigger event, then conditions, waits and actions wired together by edges.

The automation object ​

json
{
  "id": "…",
  "name": "Welcome new signups",
  "description": "",
  "trigger_event": "form.submitted",
  "graph": { "nodes": [ ], "edges": [ ] },
  "is_active": true,
  "status": "published",
  "created_at": "…",
  "updated_at": "…"
}

Two flags, and both are required to run ​

FlagQuestion
status — draft / publishedIs it finished?
is_active — true / falseIs it switched on?

The worker requires both. They are independent: publishing does not switch an automation on, and pausing does not unpublish it.

The graph ​

json
{
  "nodes": [
    { "id": "t1", "type": "trigger", "position": { "x": 0, "y": 0 } },

    { "id": "c1", "type": "condition", "position": { "x": 0, "y": 120 },
      "conditions": [
        { "field": "data.values.plan", "operator": "equals", "value": "pro" }
      ] },

    { "id": "w1", "type": "wait", "position": { "x": 0, "y": 240 },
      "wait": { "amount": 2, "unit": "hours" } },

    { "id": "a1", "type": "action", "position": { "x": 0, "y": 360 },
      "action": {
        "type": "send_email",
        "template_id": "…",
        "subject": "Welcome",
        "to": "{{ data.values.email }}",
        "from": "hello@example.com"
      } }
  ],
  "edges": [
    { "id": "e1", "source": "t1", "target": "c1" },
    { "id": "e2", "source": "c1", "target": "w1", "branch": "true" },
    { "id": "e3", "source": "w1", "target": "a1" }
  ]
}

Field names to get right

Edges use source and target, not from/to. A node's settings sit at the node's top level under conditions, action or wait — there is no settings wrapper.

position persists the dashboard canvas layout. It is required structurally, so send something even if you never open the editor.

Node types ​

TypeSettings keyContents
trigger—Exactly one per graph.
conditionconditions1–20 rows, ANDed.
waitwait{ "amount": <positive number>, "unit": "seconds" | "minutes" | "hours" | "days" }
actionactionSee below.

Conditions ​

json
{ "field": "data.values.plan", "operator": "equals", "value": "pro" }

field is a dot-path into the event envelope. operator is equals (the default if omitted), not_equals, contains or exists. exists takes no value; the others need a scalar one.

Finding the right dot-path

Paths start at data, which is the event's own payload. A form.submitted event carries data.form, data.submission_id, data.fields (the ordered list) and data.values — a convenience map so you can write data.values.Email without scanning the list. values is keyed by the field's label, which defaults to its key. A contact.* event carries data.contact, so a custom property is data.contact.properties.plan.

The quickest way to see the real shape is to point a webhook at a request-inspection endpoint and fire the trigger once.

All rows on a node must match to take its branch: "true" edge; otherwise the branch: "false" edge runs.

Actions ​

typeRequired settings
send_emailtemplate_id, subject, to, from
send_channel_messagechannel_id, content
call_urlurl (absolute http(s))

to and every other string setting accept {{ dot.path }} placeholders resolved against the triggering event.

from must be an address on a verified domain. Only the shape is checked when you save — whether the domain is verified is settled at execution time, where the action is recorded as skipped if it is not. The same is true of a template_id that names a draft.

Traversal ​

Execution walks breadth-first from the trigger, children in edge order. Condition nodes route to their branch; action nodes execute and fan out over all outgoing edges; wait nodes pause the branch and resume later. Revisits are suppressed, so a diamond join runs once, and a failing action does not stop the nodes downstream of it.

Caps ​

40 nodes, 80 edges, 20 conditions per condition node, and 100 traversal steps per run.

Create ​

POST /v1/automations
FieldRequiredNotes
nameyesUp to 255 characters.
trigger_eventyesAn event from the catalog.
graphnoDefaults to empty.
descriptionno
statusnoDefaults to published.
is_activenoDefaults to true.

The create default is the opposite of a template's

A client that POSTs a complete automation expects it to run, so create defaults to published. It is the dashboard editor that asks for a draft. Templates default to draft for the opposite reason.

Trigger events ​

form.submitted · contact.created · contact.updated · contact.deleted · contact.unsubscribed · contact.resubscribed · contact_import.completed · email.queued · email.scheduled · email.delivered · email.failed · broadcast.sent · domain.created · domain.verified · domain.verification_lost · domain.deleted · channel_message.sent · channel_message.failed

An unrecognised value is refused with the full list in the message.

Update ​

PATCH /v1/automations/{id}

Every field is optional, and omission means "keep the stored value" — which matters more here than elsewhere:

  • Omitting status lands on the stored one.
  • Omitting graph publishes the stored graph.

So an update that publishes an incomplete automation is refused with 422 notifyzr.automation.error.incomplete_graph rather than quietly unpublishing it.

Validation: draft versus published ​

AutomationGraph is validated at two levels.

LevelApplies toChecks
IntegrityAlways, drafts includedUnique ids, edges landing on nodes that exist, valid branch labels, no cycles, the size caps
CompletenessPublished onlyExactly one trigger, at least one action, every node reachable, and every node's settings filled in

That is the point of a draft: half-built work has somewhere to live, while still being a graph the editor can read back and the worker could not choke on.

The engine re-validates defensively when it runs, so a row edited outside the API cannot reach the executor.

Runs ​

GET /v1/automations/{id}/runs

Paginated. A run is recorded whenever an action node was reached — a trigger that fell down a false branch to nothing leaves no run.

json
{
  "runs": [{
    "id": "…",
    "trigger_event": "form.submitted",
    "event_id": "…",
    "status": "success",
    "actions_total": 2,
    "actions_failed": 0,
    "error": null,
    "actions": [
      { "node_id": "c1", "node_type": "condition", "type": null, "status": "matched", "reason": null },
      { "node_id": "a1", "node_type": "action", "type": "send_email", "status": "sent", "reason": null }
    ],
    "created_at": "…"
  }],
  "pagination": { "page": 1, "per_page": 25, "total": 1, "total_pages": 1 }
}

actions carries one entry per visited node, which is what makes a run diagnosable: a run that did nothing tells you which condition sent it down the false branch, and a skipped action tells you why — a draft template, an unverified sender, a suppressed recipient.

Metrics ​

GET /v1/automations/{id}/metrics?range=7d
GET /v1/automations/metrics?range=7d

range is 24h, 7d or 30d. The first is one automation; the second is across all of them.

json
{
  "range": "7d",
  "granularity": "day",
  "series": [ ],
  "totals": { "runs": 340, "runs_success": 331, "messages": 340, "messages_delivered": 336 },
  "actions": [ ],
  "failures": [ ]
}

The loop guard ​

Events caused by an automation action carry the originating automation's id, and the engine skips them. That is what makes it safe to trigger on email.* without an automation feeding itself forever.

It is a blanket rule: no automation triggers on any automation's output, so you cannot chain two automations by having one react to what the other sent.

Quota ​

Automation runs count against a monthly quota, checked per automation rather than once per event — so one event fanning out to several automations stops at the ceiling rather than overshooting it. Once over, matched automations are skipped with no actions and no run recorded until the count resets.

Released under the Apache 2.0 License.