Flows
Flows are SendSeven's visual automation engine. You design a journey on a canvas — a graph of nodes connected by edges — and the engine executes it for each contact you start the flow on. A single flow can send a message, wait for a reply, branch on conditions, update tags, hand off to a human agent, call an external API, or chain into another flow.
Flows are useful for things like:
- Onboarding sequences — welcome message, wait 1 day, send a tutorial link, branch on whether the link was clicked.
- Drip campaigns — send 5 educational emails over 2 weeks with branching based on engagement.
- Bot-style conversations — collect a contact's name and preferences via Telegram/WhatsApp, then route them to the right team.
- Reactivation flows — for contacts who haven't replied in 30 days, send a discount code and tag the engaged ones.
- Birthday / anniversary greetings — use the
scheduletrigger, which supports cron-style and birthday-anchored rules. For a single message without flow logic, the Scheduler API is simpler.
Flows execute in the same multi-channel pipeline as the rest of the platform. Sends produce real messages, count toward usage-based billing, fire the normal message webhooks, and are visible in conversations once a contact replies.
Required scopes
| Scope | Purpose |
|---|---|
flows:read | List flows, view definition, view runs and run steps |
flows:write | Create, update, archive flows; start runs from the API |
flows:publish | Move a flow from draft to published |
Flow lifecycle
| Status | Description |
|---|---|
draft | Editable. Cannot start runs from this status. |
published | Active. New runs can be started; in-flight runs continue executing. |
paused | New runs are blocked. In-flight runs continue running until they finish or are stopped. |
archived | Hidden from default lists. Cannot start runs. |
A flow's definition (nodes + edges + trigger config) is mutable on draft and published flows. In-flight runs use a snapshot of the definition taken when they started, so contacts who are already partway through continue on the version they started with; only new runs see the latest version. Exceptions: a run waiting on a step that the new version removes is canceled (flow_redefined), and exit and stop settings apply to every run immediately. Preview the impact with POST /flows/{flow_id}/edit-impact, see Editing a live flow.
Run lifecycle
| Status | Description |
|---|---|
running | Engine is actively executing nodes for this contact. |
waiting | Run is parked on a delay, a wait-for-reply, a wait-for-click, or a condition recheck. |
completed | Run reached a terminal state (no outgoing edges from the last node). |
failed | A node returned an unrecoverable error. The full traceback is stored on flow_runs.error_text. |
canceled | Ended before completion: stopped via the API, the STOP keyword, an agent reply, a closed conversation, or the contact unsubscribing. context.canceled_reason holds the reason (see Cancel reasons). |
Anatomy of a flow
A flow definition is a JSON document with three top-level keys:
{
"schema_version": 1,
"trigger": {
"kind": "manual",
"event_name": null
},
"entry_node_id": "node_welcome",
"nodes": [
{ "id": "node_welcome", "type": "send", "config": { ... } },
{ "id": "node_wait", "type": "delay", "config": { "duration_seconds": 60 } },
{ "id": "node_followup", "type": "send", "config": { ... } }
],
"edges": [
{ "from": "trigger", "to": "node_welcome", "branch": "default" },
{ "from": "node_welcome", "to": "node_wait", "branch": "default" },
{ "from": "node_wait", "to": "node_followup", "branch": "default" }
]
}
Trigger: how runs are started. See Triggers and the API.
Nodes: the work each step does. See Node reference.
Edges: directed connections between nodes. The branch key specifies which branch of a conditional node the edge belongs to (default, yes, no, or a named branch).
Creating a flow
In the app, Flows → New flow opens a four-step wizard:
- Start — begin with a blank canvas or one of the built-in templates.
- Basics — name the flow.
- Channels — pick the flow's default channels (only channels you have connected are offered).
- Trigger — select and configure what starts the flow (see Triggers and the API).
Via the API, the same happens in one call — POST /api/v1/flows with name, definition, and optionally default_channel_chain, default_language, add_detected_languages and the exit settings (stop_on_agent_reply, stop_on_conversation_closed, exit_on_unsubscribe, exit_on_unsubscribe_list_ids).
Default channel chain
Every flow carries a flow-level default_channel_chain — an ordered list of channel types (e.g. ["whatsapp", "telegram", "sms"]) that every Send, Ask-with-Buttons, and Open Conversation node falls back to when it has no chain of its own. The engine walks the chain and delivers on the first channel the contact is reachable on (see Channel persistence).
Choose your channels deliberately. Each channel supports a different feature set (buttons, media, templates, message length). A flow that targets many channels is limited to the intersection of their capabilities, so only include channels you definitely want to support — fewer channels means more rich features are available everywhere.
API behavior on POST /flows:
emailis never a valid default-chain entry and is silently stripped — email sends are configured per-node.- If you omit
default_channel_chainand the definition contains interactive nodes, the chain is auto-seeded with all non-email channel types. - If you supply a chain that is empty (or email-only) for a flow with interactive nodes, the request is rejected with
422flow_definition_invalidandreason: "channel_chain_required"(legacy_codeFLOW_CHANNEL_CHAIN_REQUIRED; before November 2026 this was a400).
Re-entry policy
When a flow is triggered for a contact who already has an active run, the engine consults the flow's re_entry_policy:
| Policy | Behavior |
|---|---|
block (default) | The new trigger is silently ignored. The existing run continues. |
restart | The existing run is canceled. A fresh run starts from the beginning. |
parallel | A second run is started alongside the existing one. Use sparingly — the engine doesn't deduplicate side effects across parallel runs. |
Exit on unsubscribe
exit_on_unsubscribe decides when an active run ends because the contact unsubscribed. Set it on POST /api/v1/flows or PATCH /api/v1/flows/{flow_id} (scope flows:write); every flow response returns both fields. Changes apply to runs that are already active.
| Value | The run ends when the contact… |
|---|---|
trigger_list (default) | …leaves the list whose subscription started the run (trigger contact.subscribed, or contact.updated on a subscription change). For every other trigger this setting does nothing. |
any | …unsubscribes from any list, or opts out of a channel entirely. |
selected | …unsubscribes from one of the lists in exit_on_unsubscribe_list_ids. |
off | Never. |
Rules:
- Only unsubscribes that happen after the run started count. An older opt-out never ends a new run, and the unsubscribe that started a run (trigger
contact.unsubscribed) never ends that run. - An opt-out from a whole channel (for example a
STOPreply that is not tied to a list) counts as leaving every list. - An unsubscribe on any channel of a matching list counts.
- The run is also checked right before each outgoing message, so a run waiting on a delay never sends after the contact unsubscribed.
- The run ends as
canceledwithcontext.canceled_reason = "contact_unsubscribed"andcontext.cancel_details(list_id,channel_type,source;list_idandchannel_typecan benull).
exit_on_unsubscribe_list_ids takes 1 to 50 list ids of your workspace and is required when the mode is selected. For the other modes the ids are kept as stored, so you can switch modes without losing the selection.
Errors (422): an unknown mode, selected without list ids, more than 50 ids, or a list id that does not exist in your workspace.
{
"exit_on_unsubscribe": "selected",
"exit_on_unsubscribe_list_ids": ["5d1c8a2e-1f7b-4c1e-9a55-0c2d7e4b9f10"]
}
AI agent while a flow runs
agent_while_running decides what a conversational AI agent does in a conversation while a run of this flow is active there. Set it on POST /api/v1/flows or PATCH /api/v1/flows/{flow_id} (flows:write):
| Value | Behaviour |
|---|---|
inherit (default) | Use the agent's own flow_running_behavior (pause unless changed). |
pause | The agent stays silent until the run ends, then answers again from the next incoming message. |
stop | The agent is turned off for the conversation when the run starts. A person, or a step of the flow that hands the conversation to the agent, turns it back on. |
keep | The agent keeps answering while the run is active. |
- The incoming message that started the run is never answered by the agent, whatever the value. A flow that is set to stay quiet when an agent is active, and is skipped for that reason, did not start, so the agent answers.
- The flow's value wins over the agent's. When several runs are active in the conversation, the most restrictive mode wins (
stop, thenpause, thenkeep). - On
PATCH, omit the field to keep it; sendinherit(ornull) to go back to the agent's setting. Responses always return one of the four values. - Applies to runs from every trigger, including API, manual and Start Flow runs that are bound to a conversation.
How a run advances
For each contact, the engine processes one node at a time:
- Load the run, the snapshotted definition, and the contact.
- Execute the current node. The node resolves to one of:
- continue — advance to the next node immediately.
- wait — pause on a timer, reply, click, or condition recheck.
- terminal — mark the run
completed. - error — mark the run
failed.
- Record an audit entry for the step.
- If
continue, loop back to step 1 with the next node. Ifwait, the run pauses and resumes automatically when the wait fires.
Each step runs exactly once and every side effect is idempotent, so a run can never double-execute a node even if processing is retried.
Webhooks
Flow runs do not emit their own customer webhook events (there are no subscribable flow.run.* events). The messages a flow sends produce the normal message webhooks. To follow a run, poll GET /api/v1/flows/{id}/runs/{run_id}; see Tracking runs.