Node Reference
Every node type accepts a config object that's specific to its purpose. Common fields like id, type, and position (for the canvas) are stripped before execution.
Common fields
Every node — regardless of type — also carries:
| Field | Type | Notes |
|---|---|---|
display_name | string | Human-readable label shown on the canvas and in the jump-to-node edge picker. Case-insensitive unique within a flow; the validator rejects publishes with duplicate display names. Defaults to a type-derived placeholder ("Send 1", "Branch 2", etc.) until the user edits it. |
The display name is what you pick from when wiring jump-to-node edges (a non-linear routing edge that targets a named node anywhere in the flow), so giving nodes meaningful names makes complex flows much easier to maintain.
"Copy node ID" was removed from the node context menu in May 2026 — referencing nodes by display_name is now the supported pattern for both UI wiring and operator readability.
Send
Sends a message to the contact across one or more channels with channel-specific content.
| Field | Type | Notes |
|---|---|---|
channel_chain | string[] | Ordered list of channels to try. ["whatsapp", "telegram", "email"] means try WhatsApp first; if the contact has no WhatsApp method or it fails, fall back to Telegram, then email. |
channel_content | object | Per-channel payloads. Each key is a channel type; each value is the message body for that channel. |
body_text | string | Legacy / fallback. Used if channel_content is empty. |
subject | string | Email only. Required for email sends. |
email_mailbox_id | uuid | Email only. Picks the From mailbox. |
template_id | uuid | WhatsApp only. References an APPROVED WhatsApp template. |
template_variables | object | WhatsApp template variable values, including variable_fallbacks for nullable fields. |
header_image_url | string | WhatsApp template header image. Pre-uploaded to Meta as a 30-day media ID. |
include_signature | boolean | Email only. Default true. Appends the user's HTML signature. |
Variable substitution. The body, subject, and template parameters all support {{contact.first_name}}, {{contact.email}}, {{vars.<flow_var>}}, {{flow.var.<name>}}, and {{flow.stop_url}}. Unresolved variables fall back to per-parameter variable_fallbacks (WhatsApp templates) or empty strings.
Conversation behavior. By default, flow sends do NOT open a conversation in the inbox. The first inbound reply attaches the prior 30 days of orphan flow messages to a new conversation (configurable per flow via inbound_behavior).
Delay
Pauses the run for a fixed amount of time.
| Field | Type | Notes |
|---|---|---|
duration | integer | Wait duration, in the chosen unit. |
unit | "seconds" | "minutes" | "hours" | "days" | Time unit. The UI surfaces all four; the engine normalizes to seconds internally. |
duration_seconds | integer | Read-only effective value, computed from duration × unit. Max 14 days (1209600 seconds). |
Standalone Delay node cap. A standalone Delay node is capped at 30 seconds. For longer waits, use a Condition node with wait_max_seconds (which is purpose-built for long-running waits and uses the tiered recheck cadence) — a standalone Delay tying up engine resources for hours or days would push backpressure onto the scheduler.
Inter-message delay inside Send. When a Send node carries 2 or more messages, the per-message delay is capped at 10 seconds (_SEND_MESSAGE_DELAY_MAX_MULTI = 10 on the server) to avoid tripping provider throttling on WhatsApp / Telegram bursts. The validator enforces this at save time; values above 10 s are rejected with an explanatory error.
Internally, the engine creates a scheduled_sends-style row with next_run_at = now + duration. The scheduler tick republishes a resume task when the deadline passes.
Collect Input
Sends a prompt and waits for the contact's next inbound message. The reply is stored in a flow variable.
| Field | Type | Notes |
|---|---|---|
prompt | string | Message text shown to the contact. Same variable substitution as Send. |
output_var_name | string | Flow variable to write the reply into. Reference downstream as {{vars.<name>}}. |
timeout_seconds | integer | Optional. If no reply arrives within this window, the engine takes the timeout branch (configured via an outgoing edge with branch: "timeout"). |
channel_chain | string[] | Same as Send — chooses where to send the prompt. |
While a Collect Input node is waiting, replies on the same channel are not routed to a conversation in the inbox — they go to the flow. This makes flows usable as bot builders. Replies received during a Collect Input wait are still persisted as messages rows.
Condition (Branch)
Branches the flow based on a typed rule set. Optionally waits up to wait_max_seconds for the condition to become true (e.g., wait for a click, then take the YES branch).
| Field | Type | Notes |
|---|---|---|
rules | Rule[] | Each rule has field, operator, value. See operator list below. |
rules_op | "all" | "any" | How rules combine. Default all. |
wait_max_seconds | integer | Optional. 0 (default) = evaluate once. >0 = re-evaluate periodically; if true, take YES; if still false at the deadline, take NO. |
Edges from a Condition node carry branch: "yes" or branch: "no". Both must be present for the flow to publish.
Typed value picker. The value side of a rule can be picked from a typed dropdown sourced from GET /api/v1/flows/variables?flow_id={uuid}. The dropdown groups options into:
- Contact fields (built-in:
first_name,email,language, …) - Custom fields (your tenant's contact custom fields)
- Tags
- Lists
- Flow variables (any
collect_input,kb_query, orcreate_tracked_linkoutput declared upstream in this flow)
Free-text values are still accepted — the picker is a convenience layer on top, not a constraint. See the variables endpoint for the underlying schema.
Operators:
| Operator | Applies to | Description |
|---|---|---|
equals / not_equals | any field | Exact string match. |
contains / not_contains | strings | Substring match. |
is_empty / is_not_empty | any field | NULL / empty-string check. |
in_list / not_in_list | list-typed fields | Tag membership, list membership. |
link_clicked | flow.var.<tracked_link_id> | True once tracked_links.click_count > 0. Excludes bot/preview crawlers — see Tracked links. |
language_in / language_not_in | contact.language + contact.languages | Matches primary language OR auto-detected language list. |
Tiered recheck cadence. When wait_max_seconds > 0, the engine re-evaluates the condition on a tiered schedule: every 10s for the first 10 minutes, every 30s for the next 2 hours, every 60s for the next 24 hours, then every 5 min until the deadline. This keeps fast feedback for click-tracking while remaining cheap for long waits.
Deadline anchoring. wait_max_seconds is anchored on the FIRST evaluation, not on each recheck. A 10-minute timeout fires 10 minutes after the condition node first ran, regardless of how often it has been rechecked.
Tag / Update Contact
Mutates the contact record without sending a message.
| Field | Type | Notes |
|---|---|---|
add_tags | string[] | Tag names to add. Created if they don't exist. |
remove_tags | string[] | Tag names to remove. |
set_fields | object | Map of contact-field name → value. Supports built-in fields (first_name, last_name, email, phone, etc.) and custom fields. |
subscription_changes | object[] | Each entry: { "channel_type": "whatsapp", "status": "subscribed" | "unsubscribed" }. Routes through SubscriptionService (audit-logged). |
Open Conversation
Creates (or reuses) a real conversation row for the contact on a chosen channel. The conversation appears in the inbox and follows your normal assignment rules.
| Field | Type | Notes |
|---|---|---|
channel_type | string | Channel to open the conversation on. Must match a channel the contact has a method for. |
internal_note | string | Optional. Posted as a tenant-internal note on the new conversation. Variable substitution supported. |
assign_user_id | uuid | Optional. Direct-assigns the conversation to a team member. |
assign_team | string | Optional. Routes to a team's queue. |
Mostly used for "send 3 messages → if no click, open a conversation so an agent can intervene".
Create Tracked Link
Generates a short URL (https://c.sendseven.com/<code>) that redirects to a target URL and counts clicks. The short URL is written into a flow variable for use in downstream Send nodes.
| Field | Type | Notes |
|---|---|---|
target_url | string | Where the short URL redirects to. Variable substitution supported. |
output_var_name | string | Flow variable to write the short URL into. Default tracked_link_url. |
The redirect handler filters bot/preview User-Agents (WhatsApp, Facebook, Telegram, Slack, iMessage, Discord, generic crawlers, headless browsers). Bot fetches go to tracked_links.preview_count; only real-human GETs increment click_count. This means the Condition link_clicked operator stays false until a human clicks — link-preview crawls don't accidentally fire YES.
Webhook
Calls an arbitrary HTTPS endpoint and continues based on the response.
| Field | Type | Notes |
|---|---|---|
url | string | HTTPS URL. HTTP allowed only for localhost / *.local. Private-IP literals are rejected. |
method | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | Default POST. |
headers | object | Static request headers. |
body | object | string | Request body. Variable substitution supported. |
timeout_seconds | integer | Default 10. Max 30. |
success_codes | integer[] | HTTP codes that count as success. Default [200, 201, 202, 204]. |
Edges can branch on success or error — the engine inspects the response status and takes the matching branch.
Run AI Assistant
Hands the conversation off to an AI Assistant (a Knowledge-Base-backed bot) for a bounded conversational sub-loop. The bot answers the contact's messages until one of three exit conditions fires, then the flow resumes on the matching branch.
| Field | Type | Notes |
|---|---|---|
bot_id | uuid | The Assistant to run. Must be active. |
max_turns | integer | null | Maximum number of user→bot exchanges before the completed branch fires. null = unlimited (use with care). Valid range: 1..200. |
allow_stop_keyword | boolean | If true, the contact can type a stop keyword (e.g., "human", "agent") to short-circuit the loop and take the escalated branch. Uses the bot's configured escalation keywords. |
timeout_seconds | integer | If no contact reply arrives within this window, the timeout branch fires. No upper cap on the server side, but keep it within the channel's session window (24h for WhatsApp) to avoid template-send requirements on resume. |
escalation_threshold | float | Confidence below which the bot escalates. Persisted as 0.0..1.0; the UI surfaces it as a 0..100 % slider. |
Required branches. The node must wire three outgoing edges:
| Branch | Fires when |
|---|---|
completed | The bot answered the user's question, hit max_turns, or the bot itself decided it was done. |
timeout | No user reply within timeout_seconds. |
escalated | The bot's confidence dropped below escalation_threshold, or the contact used a stop keyword (when allow_stop_keyword is on). |
Missing branches surface as publish-time warnings — the flow can be saved without all three wired, but POST /publish will warn about the unrouted exits. Adding all three is required for predictable production behavior.
Current limitation: the bot integration service does not yet count turns. Setting max_turns <= 1 is treated as "cap reached on turn 1" — the loop exits via completed after the first reply. If you need a strict one-shot bot response, this is the right setting; if you intended multi-turn behavior, set max_turns >= 2 once turn counting lands.
Stop / Terminate
Explicitly ends the run. Useful for "if condition X, stop here" branches that should not fall through to the rest of the flow.
| Field | Type | Notes |
|---|---|---|
reason | string | Optional. Stored on flow_runs.stop_reason for analytics. |