Automations
Build visual flows that trigger on social events and orchestrate multi-step conversations across Instagram, Messenger, WhatsApp, and Telegram.
Overview
Automations are RelayAPI's flow-based engine. A flow reacts to events on a connected social channel — comments, DMs, keywords, scheduled times, tag changes, third-party webhooks — and executes a directed graph that can send messages, capture replies, branch on conditions, mutate the contact, call external APIs, and hand control off to another flow.
Supported channels: Instagram, Facebook Messenger, WhatsApp, and Telegram. Each automation is bound to exactly one channel; the builder surfaces channel-aware warnings when you use a feature that channel doesn't support.
Core concepts
An automation has four moving parts:
- Graph — the flow itself. Stored as a single JSONB document on the automation row (
nodes,edges,root_node_key). Nodes have typed input and output ports; edges connect one node's output port to another node's input port. No label fallbacks, no implicit wiring — if a port has no outgoing edge, the run ends there. - Entrypoints — the events that start a run. A flow can have multiple entrypoints (several keywords, a DM kind, a webhook) and the matcher picks the most specific one on each event.
- Bindings — channel-surface attachments on a social account. There are five:
default_reply,welcome_message,conversation_starter,main_menu, andice_breaker. Bindings run a flow when an entrypoint doesn't match. - Runs — execution instances. One row per enrolled contact per flow, with append-only per-node logs in
automation_step_runsfor inspection.
Edges are port-based
Every edge connects four things:
{ "from_node": "greet", "from_port": "button.btn_large", "to_node": "order_large", "to_port": "in" }There is no label field and no fallback — the port key IS the routing primitive. A message node with two branch buttons exposes button.<id> output ports; the runner follows the edge matching the port the handler exited through. If you don't wire a button port, pressing that button ends the run on that path. Ports are derived deterministically from node config on every save, so adding a button creates its port, deleting one removes the port and prunes orphan edges with a warning.
Lifecycle
draft → active → paused → archived- draft — initial state for newly created automations; nothing fires.
- active — entrypoints and bindings match live events.
- paused — no new runs start; in-flight runs keep going unless stopped.
- archived — read-only; no runs start.
There is no revisions / draft-published split. Every automation has exactly one live graph. Edits are instantly live. If an in-flight run's current node disappears after an edit, the run exits cleanly with exit_reason = "graph_changed" — it never corrupts state.
Quickstart
The fastest path is a template. One POST creates the flow, generates the graph, and attaches an initial entrypoint:
curl -X POST https://api.relayapi.dev/v1/automations \
-H "Authorization: Bearer rlay_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Link-in-bio giveaway",
"channel": "instagram",
"template": {
"kind": "comment_to_dm",
"config": {
"post_ids": ["17895..."],
"keywords": ["link", "giveaway"],
"public_reply": "Sent!",
"dm_message": { "blocks": [{ "id": "b1", "type": "text", "text": "Here you go, {{contact.first_name}}!" }] },
"once_per_user": true
}
}
}'The call returns the full automation with its generated graph as a draft. Inspect, tweak if needed via PUT /v1/automations/{id}/graph, then activate with POST /v1/automations/{id}/activate.
See Templates for the full catalog and per-template config shapes.
Authoring a custom graph
For anything a template doesn't cover, create a blank automation and replace the graph:
# 1. Create
POST /v1/automations
{ "name": "Welcome + ask email", "channel": "instagram", "template": { "kind": "blank", "config": {} } }
# 2. Replace graph
PUT /v1/automations/{id}/graph
{
"graph": {
"schema_version": 1,
"root_node_key": "greet",
"nodes": [
{
"key": "greet",
"kind": "message",
"config": {
"blocks": [{ "id": "b1", "type": "text", "text": "Hey {{contact.first_name}}! Want my free guide?" }],
"wait_for_reply": false
},
"ports": []
},
{
"key": "ask_email",
"kind": "input",
"config": { "prompt": "Drop your email and I'll send it over.", "input_type": "email", "save_to_field": "email" },
"ports": []
}
],
"edges": [
{ "from_node": "greet", "from_port": "next", "to_node": "ask_email", "to_port": "in" }
]
}
}
# 3. Add an entrypoint
POST /v1/automations/{id}/entrypoints
{ "kind": "dm_received", "channel": "instagram", "social_account_id": "acc_abc123" }
# 4. Activate
POST /v1/automations/{id}/activateOn save, the server derives each node's ports array, runs the validator, and stores the canonicalized graph. Validation errors block activation and auto-pause the automation; warnings (orphan button ports, etc.) don't.
Merge tags
Text blocks, button labels, input prompts, and webhook bodies all support {{...}} substitution:
{{contact.first_name}},{{contact.email}},{{contact.phone}},{{contact.custom_fields.<slug>}}{{context.<key>}}— values captured earlier in the same run (input replies, HTTP response extracts, webhook payload mappings){{run.id}},{{run.started_at}}{{account.name}},{{account.handle}}
Unknown paths surface as warnings at save time; unresolved tags at runtime render as an empty string.
Availability notes
- AI nodes — dropped from v1.
ai_step,ai_agent, andai_intent_routerare archived and will return in a later release. - Stubbed bindings —
conversation_starter,main_menu, andice_breakerare stored and configurable in the UI in v1 but do not yet push to the platform. Platform sync lands in v1.1. - Broadcasts are a separate system and are not part of automations.
Related
- Nodes — the 10 node kinds, ports, config shapes, action catalog.
- Triggers — entrypoint kinds, bindings, conflict resolution, webhook entrypoints.
- Runs — run lifecycle, step-run inspection, stopping a run.
- Templates — the 8 starter templates and their config shapes.
Found something wrong? Help us improve this page.