Docs

Automation Studio

The visual canvas that powers every automation. Drag, connect, condition, send.

The canvas

The Studio is an xyflow canvas with three panes — palette, canvas, inspector. Drag a node from the palette onto the canvas, connect handles, click any node to edit its config in the right rail. Save (⌘S) commits the graph; status stays DRAFT until you activate.

Node reference

KindPurposeRequired config
TRIGGEREntry point. One per automation.triggerType (MANUAL / DUE_DATE / APPOINTMENT_TIME / RENEWAL_DATE / WEBHOOK). Date-based triggers also need source ({kind: CONTACT_METADATA, key} | CONTACT_CREATED_AT | WEBHOOK_PAYLOAD), offsetDays, timeOfDay, timeZone. Webhook triggers get a minted URL, optional HMAC secret, and payload-mapping config.
SEND_MESSAGESend to one or many contacts across one or many channels.recipients (TRIGGER_CONTACT | SEGMENT | TAG_QUERY | AD_HOC), channels[] of {channel, templateId}, multiChannelMode (CASCADE | BROADCAST)
WAITPause execution for a fixed duration.delayMinutes
CONDITIONBranch on a predicate.kind (HAS_TAG / HAS_EMAIL / HAS_PHONE / FIELD_EQUALS / METADATA_EQUALS) + parameters
SWITCHRoute to multiple branches by rule set.rules[] (field/key/operator/value/handleLabel), fallbackHandleLabel
MERGERe-join branches into one path.
SETCompute templated values for downstream steps. No I/O.mode (MERGE | REPLACE), assignments[] of {target, value, type}
STOP_ERRORAbort the run with a human-readable reason.message (1–500 chars)
INTEGRATIONCall an external system (Google Sheets / HTTP).provider + provider-specific config
ENDTerminal node. Required.

SEND_MESSAGE — recipients & channel cascade (W5.2)

SEND_MESSAGE now drives both who receives and how they receive in a single node. Two inspector panels in the studio:

  • Recipients — pick one of four modes: TRIGGER_CONTACT (legacy single-recipient — the run's target), SEGMENT (every contact matching a saved segment's filter), TAG_QUERY (ad-hoc match-ALL + match-ANY tag predicate), AD_HOC (manually-curated contactId list, capped at 2,000).
  • Channels — ordered list of up to 6 (channel, templateId)pairs. multiChannelMode toggles between CASCADE (try each in order, stop on first success per contact — lowest cost, no double-message) and BROADCAST (dispatch on every channel the contact has data for — maximum reach).

Channel options now include the six social DM platforms — Instagram, TikTok, Facebook Messenger, LinkedIn, X, Threads — alongside Email, SMS, WhatsApp, Voice, and Telegram. Social DM dispatch is provider-stubbed; the DeliveryAttempt row carries the recipient handle from {{contact.socials.<platform>}} and per-platform Meta Graph / X / LinkedIn wiring ships in follow-ups. Each social platform also gets a first-class render variable: {{instagram_handle}}, {{tiktok_handle}}, {{messenger_handle}}, {{linkedin_handle}}, {{x_handle}}, {{threads_handle}}.

Date-driven triggers (G3)

DUE_DATE, APPOINTMENT_TIME, and RENEWAL_DATE all share the same inspector. Pick where the date lives, how many days to shift, the time of day to fire, and the IANA timezone the time is interpreted in. The fire instant is computed per contact as source + offsetDays @ timeOfDay (timeZone).

  • Source = Contact metadata field — read {{contact.metadata.<key>}}. The inspector offers an autocomplete datalist of your workspace's most-used keys (top 50 by usage) so operators don't have to recall their CSV column names.
  • Source = Contact created at — built-in. Useful for “X days after signup” flows.
  • Source = Webhook payload field — paired with a WEBHOOK-trigger workflow that ingests a date inside the payload (e.g. Stripe's subscription period end). The webhook ingestion path resolves the date at request time — the periodic sweeper does NOT scan contacts for this source kind.

A periodic sweeper (runs alongside the recurring-schedule poller) finds contacts whose fire instant just landed in the (lastSweep, now] window and creates one WorkflowRun per match. A per-(workflow, contact, fireAt) unique key gives idempotency: concurrent sweepers can race without double-firing. Freshly activated workflows only sweep forward from workflow.updatedAt; the lookback caps at 30 days so operators can't accidentally fan out years of historical contacts.

Webhook triggers (G4)

Set the trigger source to Inbound webhook and the inspector mints POST /v1/webhooks/in/<token>. The body of every accepted POST starts a WorkflowRun targeted at the contact the payload identifies. See Inbound webhooks for realistic Tally / Stripe / Shopify / Typeform examples and the full signing + payload-mapping reference.

Transform nodes (W5)

Two node kinds shipped in W5 (2026-05-30) close the “the data shape doesn't match my template” gap and the “I want this run to fail loudly when a precondition isn't met” gap.

  • SET (Set fields) — writes templated values into the per-step output bag so downstream nodes can render {{step.<id>.<field>}}. No credentials, no retries; the graph walker dispatches inline. The schema BLOCKS targets that match contact.marketingConsent /contact.transactionalConsent / contact.consent*— the consent capture path is the only way to flip a consent field.
  • STOP_ERROR (Stop & Error) — explicit failure terminator. Marks the run FAILED with the configured message, bypasses retries — the abort is intentional. Pair with a CONDITION's false branch to fail-fast with a human-readable reason that lands in the Run history (/runs) and the audit log.

Edges & handles

Most nodes have a single output. CONDITION has two outputs labelled yes and no. Each output handle exposes a unique sourceHandle string so the runtime can decide which branch to follow.

Condition branches

example edges
[
  { "id": "e1", "source": "trigger-1", "target": "send-1" },
  { "id": "e2", "source": "send-1",    "target": "cond-1" },
  { "id": "e3", "source": "cond-1",    "sourceHandle": "yes", "target": "end-1",  "label": "replied" },
  { "id": "e4", "source": "cond-1",    "sourceHandle": "no",  "target": "send-2", "label": "no reply" },
  { "id": "e5", "source": "send-2",    "target": "end-1" }
]

Graph shape

The whole graph is stored as JSON on the Automation row. Same shape is accepted by the Workflow table. The shared validation lives in workflowGraphSchema.

POST /automations body
{
  "name": "Welcome series",
  "triggerKind": "MANUAL",
  "graph": {
    "nodes": [
      { "id": "t", "kind": "TRIGGER",       "label": "Manual start", "config": { "triggerType": "MANUAL" }, "position": { "x": 0,   "y": 200 } },
      { "id": "s", "kind": "SEND_MESSAGE",  "label": "Welcome",     "channel": "WHATSAPP",
        "config": { "templateId": "tpl_abc" }, "position": { "x": 240, "y": 200 } },
      { "id": "e", "kind": "END",           "label": "Done",        "config": {}, "position": { "x": 480, "y": 200 } }
    ],
    "edges": [
      { "id": "e1", "source": "t", "target": "s" },
      { "id": "e2", "source": "s", "target": "e" }
    ]
  }
}

Validation

  • Exactly one TRIGGER node, ≥ one END node.
  • Every non-TRIGGER node must have an incoming edge.
  • Every non-END node must have an outgoing edge.
  • Every SEND_MESSAGE must reference a templateId at activate time.
  • Every SEND_MESSAGE.channel must be in your plan's allow-list.

Keyboard shortcuts

KeyAction
⌘SSave current graph
Delete / BackspaceDelete selected node or edge
EscDeselect