Skip to main content

Flow format spec

This page describes every part of a flow definition: node types, their fields and allowed values, condition operators, variables, edges and triggers. It is generated from the same rules the API uses to validate flows, so a definition that follows it saves cleanly.

Use it to:

  • read a flow as JSON with GET /api/v1/flows/{flow_id} (the definition field),
  • create or update flows with POST /api/v1/flows and PATCH /api/v1/flows/{flow_id} (same validation as a save in the builder),
  • let an AI write the JSON for you (copy the prompt at the end).

Top-level shape​

{
"schema_version": 1,
"trigger": {
"type": "keyword_match",
"config": {
"keywords": [
"hi"
],
"match_mode": "contains"
}
},
"entry_node_id": "first_step",
"nodes": [
{
"id": "first_step",
"type": "send",
"display_name": "Greeting",
"config": {}
}
],
"edges": [
{
"from": "first_step",
"to": "next_step",
"branch": "default"
}
]
}
KeyNotes
schema_versionAlways 1.
trigger{type, config}. See Triggers.
entry_node_idThe first action node. It must not be a trigger node.
nodesArray of {id, type, config}. id is any unique string.
edgesArray of {from, to, branch}. branch must be a label of the source node type.
inbound_behaviorignore / stop_and_open_conversation (what an inbound message does while the run waits).
allow_stop_keyword, stop_keyword_responseLet the contact stop the flow with a keyword; the response is Send-shaped.
global_timeout_enabled, global_timeout_seconds, global_timeout_node_idMove an inactive run to a node after 60..2592000 seconds.
suppress_inbound_conversationBoolean. Do not open an inbox conversation for messages that start this flow.

Per node, only id, type, config and the optional display_name (unique per flow) matter. position ({x, y}) is optional and only affects where the builder draws the node; a missing position never causes a validation error.

Limits: the definition may be at most 256 KB and nested at most 64 levels deep.

Channels are not set per node. The flow's default_channel_chain (a field of the flow, next to definition) sets the channel order, using email / instagram / live_chat / messenger / rcs / sms / telegram / whatsapp.

Node types​

Allowed type values: api_call / branch / choice / close_conversation / code / collect_input / condition / create_tracked_link / delay / end_flow / hubspot_set_field / kb_query / open_conversation / run_assistant / send / send_email / send_template / send_whatsapp_flow / start_flow / tag. Any other type is rejected.

api_call​

Calls an external HTTPS API and stores the response.

FieldType / allowed valuesRequiredNotes
methodDELETE / GET / HEAD / PATCH / POST / PUTyes
urlhttps URL (may use variables)yesPrivate, loopback and metadata addresses are refused.
auth{type, ...}notype: api_key / basic / bearer / none. Put credentials in {{secrets.NAME}}, never inline.
headersarray of {key, value}no
query_paramsarray of {key, value}no
body_modeform / json / none / rawno
body_jsonJSON (may use variables)no
timeout_secondsinteger 1..30no
output_varvariable namenoResponse at {{vars.<name>.status}}, {{vars.<name>.body...}}.
retry_on_errorbooleanno

Branches: default / error / success.

branch​

Multi-way switch: the first case whose rules match wins, otherwise else.

FieldType / allowed valuesRequiredNotes
casesarray (1..20) of {rules_op, rules[], label?}yesSame rule shape as the Condition node.
else_node_idnode idnoAlternative to an else edge.

Branches: case:0, case:1, ... (0-based, one per case) and else (default is an alias of else).

choice​

Asks a question with 2 to 10 buttons and routes on the picked option.

FieldType / allowed valuesRequiredNotes
promptSend-shaped object (messages[], see the Send node)yes
optionsarray (2..10) of {id, label}yesEach id becomes an edge branch.
attempt_timeout_secondsinteger 60..604800noDefault 86400.
on_timeout_node_idnode idno

Branches: One edge per option id, plus fallback (unrecognised reply) and timeout.

close_conversation​

Closes the current conversation and continues.

FieldType / allowed valuesRequiredNotes
reasonstringno

Branches: default.

code​

Runs a short sandboxed script over the flow variables.

FieldType / allowed valuesRequiredNotes
codestring, max 64 KByesNo network or file access.
timeout_secondsinteger 1..10no
declared_outputsarray of variable namesnoValues the script returns into vars.
error_varvariable nameno

Branches: default / error / success.

collect_input​

Asks a question, waits for the reply, validates it and stores it.

FieldType / allowed valuesRequiredNotes
promptSend-shaped object (messages[], see the Send node)yesThe question. Never put a channel_chain inside it.
target_fieldcontact.<column>, contact.custom.<key> or flow.var.<key>noBuilt-in columns: contact.birthday / contact.country / contact.email / contact.first_name / contact.language / contact.last_name / contact.phone.
validation{type, ...}notype: date / email / enum / file / number / phone / regex / text. For file, accepted_kinds is a subset of any / audio / document / image / video.
validation_error_messageSend-shaped object (messages[], see the Send node)noSent when the reply is invalid.
max_attemptsinteger 1..10no
attempt_timeout_secondsinteger 60..604800noDefault 86400 (24 hours).
on_timeout_node_idnode idno

Branches: default (valid answer) and timeout. With buttons: one edge per button id plus fallback.

condition​

Binary yes/no gate over one or more rules.

FieldType / allowed valuesRequiredNotes
rules_opall / anynoDefault all.
rulesarray of rule objects (non-empty)yesEach rule: field, operator, value (omitted for value-less operators), optional value_type. See Condition rules below.
wait_max_secondsinteger 0..1209600noRe-check for up to this long before taking no.

Branches: yes and no (both required at publish). Legacy aliases accepted: 0 / 1 / default / false / true.

Creates a click-tracked short link and stores it in a flow variable.

FieldType / allowed valuesRequiredNotes
original_urlURL (may use variables)yes
output_var_namevariable nameyesRead it later as {{vars.<name>}}.
link_labelstring, max 255no
expires_in_daysinteger 1..3650no

Branches: default. Use the Condition operator link_clicked on flow.var.<name> to branch on a click.

delay​

Pauses the run without blocking (scheduler based), from seconds to weeks.

FieldType / allowed valuesRequiredNotes
modetime / eventnoDefault time.
duration_secondsinteger, 0 or moreyes for timeHow long to wait.
eventreplynoevent mode only: wait for the contact's next reply.
channel_typestringnoevent mode only: only a reply on this channel type resumes.
timeout_secondsintegernoevent mode only: give up after this many seconds.

Branches: default.

end_flow​

Ends the run.

FieldType / allowed valuesRequiredNotes
reasonstringno
close_conversationbooleannoDefault false.

Branches: None (terminal).

hubspot_set_field​

Writes a property on the linked HubSpot contact.

FieldType / allowed valuesRequiredNotes
property_nameHubSpot propertyyes
valuestring (may use variables)yes
output_varvariable nameno

Branches: default / error / no_mapping / success.

kb_query​

Searches the Knowledge Base and stores the answer.

FieldType / allowed valuesRequiredNotes
querystring (may use variables)yes
folder_idsarray of folder idsno
top_kinteger 1..10no
output_varvariable nameyesRead it later as {{vars.<name>}}.
min_confidencenumberno
ask_firstbooleannoAsk the contact for the question first (uses prompt).
timeout_secondsintegerno

Branches: default / match / no_match / timeout.

open_conversation​

Opens (or reuses) a conversation in the inbox and optionally assigns it.

FieldType / allowed valuesRequiredNotes
assign_strategycontact_owner / least_busy_by_tag / least_busy_in_team / none / round_robin_all / round_robin_by_tag / round_robin_in_set / round_robin_in_team / specific_usernoDefault none.
assign_user_idteam member idfor specific_userMust be a member of this workspace.
assign_team_idinbox idfor team strategiesMust be an inbox of this workspace.
assignment_tag_idmember tag idfor skill routing
prefer_online_agentsbooleanno
add_conversation_tagsarray of tag namesno
remove_conversation_tagsarray of tag namesno
unattended_fallback_minutesinteger 1..1440noTag-based strategies only: re-route when nobody has picked the conversation up in time.

Branches: default.

run_assistant​

Hands the conversation to an AI bot or Conversational Agent until it finishes.

FieldType / allowed valuesRequiredNotes
bot_idbot idyes for a botMust be a bot of this workspace.
target{type, id}notype: agent / bot.
timeout_secondsintegerno
max_turnsinteger 1..200no
allow_stop_keywordbooleanno
output_variable_prefixidentifiernoPrefix for the values the assistant extracts.

Branches: completed / default / done / escalated / timeout. done is only valid when the target is an agent.

send​

Sends one or more messages to the contact over the flow's channel chain.

FieldType / allowed valuesRequiredNotes
messagesarray of message objectsyes (unless channel_content covers every channel)Each message: type (audio / document / image / interactive / text / video), text, order (integer, required), delay_seconds (0..15, required), optional media_url or attachment_id (required for media types at publish), media_caption, buttons[] (id, label).
channel_contentobject keyed by channel typenoPer-channel override: {"whatsapp": {"messages": [...]}}. Channels without an entry use messages.
button_wait_secondsinteger 60..604800noOnly when a message has buttons: how long to wait for a click.
timeout_enabledbooleannoDefault true. False waits for a click without a deadline.
on_timeout_node_idnode idnoWhere to continue when the button wait times out.

Branches: default (next step). With buttons: one edge per button id, plus timeout.

send_email​

Sends an email from one of the workspace's mailboxes.

FieldType / allowed valuesRequiredNotes
toemail address or variablenoDefault is the contact's email (to_source).
to_sourcestringno
subjectstring (may use variables)yes
email_mailbox_idmailbox idyes at publish
body_modestringnoPlain text, HTML or a saved email design.
body_htmlstringno
body_textstringno
email_content_idsaved email design idno

Branches: default.

send_template​

Sends an approved WhatsApp template (works outside the 24-hour window).

FieldType / allowed valuesRequiredNotes
template_idWhatsApp template idyesMust be a template of this workspace.
template_variablesobjectnoValues for the template placeholders (may use variables).
buttonsarray of {index, caption}noSnapshot of the template's quick-reply buttons.
attempt_timeout_secondsinteger 60..604800no
on_timeout_node_idnode idno

Branches: tplbtn_<index> per button, fallback, timeout, default.

send_whatsapp_flow​

Sends a WhatsApp Flow (form) and waits for the submission.

FieldType / allowed valuesRequiredNotes
whatsapp_flow_idWhatsApp Flow idyesMust be a WhatsApp Flow of this workspace.
output_keyvariable namenoSubmitted answers land under this key.
attempt_timeout_secondsinteger 60..604800no

Branches: completed / default / fallback / timeout.

start_flow​

Starts another flow of this workspace, optionally waiting for its result.

FieldType / allowed valuesRequiredNotes
target_flow_idflow idyesMust be a flow of this workspace.
modewait / fire_and_forgetno
input_varsarray of {name, value}no
output_mappingarray of {from, to}nowait mode: copy the child's variables back.
result_variablevariable nameno

Branches: wait: completed / default / failed / timeout. fire_and_forget: default / failed.

tag​

Updates the contact: tags, fields, subscriptions and languages.

FieldType / allowed valuesRequiredNotes
add_tagsarray of tag namesnoTags are matched by NAME, case-insensitively.
remove_tagsarray of tag namesno
set_fieldsarray of {field, value}nofield is a built-in (birthday / company / email / first_name / language / last_name / lifecycle_stage / phone / source) or custom.<key>. value may use variables.
subscriptionsarray of {list_id, channel_type, action}noSubscribe or unsubscribe the contact.
languages_addarray of language codesno
languages_removearray of language codesno

Branches: default.

Condition rules​

Used by condition and by every case of branch.

{"field": "contact.tag", "operator": "has_tag", "value": "VIP"}
{"field": "vars.api_result.body.count", "operator": "gt", "value": "10", "value_type": "number"}

field is one of:

  • a curated field: contact.has_method / contact.tag / flow.entry_at / flow.last_button_clicked / flow.last_channel_used / flow.last_inbound / subscription,
  • a dotted field under contact. / flow.var. (for example contact.first_name, contact.custom.plan, flow.var.answer),
  • a variable path whose first part is contact / conversation / flow / message / trigger / vars (for example vars.api_result.body.count). secrets is never allowed in a condition.

An unknown namespace is rejected with a path such as nodes[route].cases[0].rules[0].field.

operator is one of after / before / contains / contains_any / ends_with / eq / equals / exists / gt / gte / has_contact_method_for_channel / has_tag / is_empty / is_false / is_not_empty / is_subscribed_to_list / is_true / language_identified / language_in / language_is / language_not / language_not_in / language_unidentified / link_clicked / lt / lte / more_than_days_ago / neq / not_contains / not_equals / not_exists / not_has_contact_method_for_channel / not_has_tag / not_one_of / not_subscribed_to_list / on / one_of / starts_with / within_last_days.

Operators without a value: exists / has_contact_method_for_channel / is_empty / is_false / is_not_empty / is_subscribed_to_list / is_true / language_identified / language_unidentified / link_clicked / not_exists / not_has_contact_method_for_channel / not_subscribed_to_list.

OperatorOnly valid on field
link_clickedflow.var.<name>
has_contact_method_for_channelcontact.has_method
is_subscribed_to_listsubscription
language_identifiedcontact.language
language_incontact.language
language_iscontact.language
language_notcontact.language
language_not_incontact.language
language_unidentifiedcontact.language
not_has_contact_method_for_channelcontact.has_method
not_subscribed_to_listsubscription

Tag rules (has_tag, not_has_tag) compare the tag NAME, case-insensitively. one_of, not_one_of and contains_any take a JSON array of strings as value. Language operators (language_identified / language_in / language_is / language_not / language_not_in / language_unidentified) take ISO codes and accept include_unidentified: true.

value_type (optional) is one of auto / boolean / date / number / string. With an explicit type, the operator must fit it and a literal value must parse as that type:

value_typeOperators
autoafter / before / contains / contains_any / ends_with / eq / exists / gt / gte / is_empty / is_false / is_not_empty / is_true / lt / lte / more_than_days_ago / neq / not_contains / not_exists / not_one_of / on / one_of / starts_with / within_last_days
stringcontains / contains_any / ends_with / eq / exists / gt / gte / is_empty / is_not_empty / lt / lte / neq / not_contains / not_exists / not_one_of / one_of / starts_with
numbereq / exists / gt / gte / is_empty / is_not_empty / lt / lte / neq / not_exists
booleaneq / exists / is_false / is_true / neq / not_exists
dateafter / before / eq / exists / gt / gte / is_empty / is_not_empty / lt / lte / more_than_days_ago / neq / not_exists / on / within_last_days

Variables​

Text fields may contain variables written as {{namespace.field}}. Paths use dots, [0] for list items and ["key"] for keys with special characters.

NamespaceContents
contactThe contact: first_name, last_name, email, phone, custom.<key>, tags, owner fields.
messageThe message that started the run (text, type).
conversationThe current conversation, including assigned_agent.
triggerThe trigger payload (for example {{trigger.order_id}} for an API event).
varsFlow variables written by steps (output_var, target_field: flow.var.<key>, ...). flow.var.<key> is an alias.
flowRun facts: last_inbound, last_button_clicked, last_channel_used, entry_at.
secretsWorkspace secrets. Only in the api_call node; never in messages or conditions.

Unknown variables render as an empty string. Variable names you create cannot be is_test / stop_flow_url / test_conversation_id / waflow and cannot start with __.

Edges and branches​

An edge is {from, to, branch}. from and to are node ids. branch selects which exit of the source node the edge belongs to; each node type lists its labels above. Most action nodes use default. At publish every reachable node must be connected and every condition needs both yes and no.

Triggers​

trigger is {type, config}. Allowed types:

TypeConfig
api_eventStarted by your API call with a payload, read as {{trigger.<key>}}.
click_to_adFirst message of a conversation that started from an ad. channels subset of instagram / messenger / whatsapp, scope any_ad / specific_ads.
comment_receivedA comment on an Instagram or Facebook post. match_mode contains / exact / starts_with, scope all_posts / specific_posts.
contact.createdA contact was created. Optional field filters.
contact.subscribedA contact subscribed to a list.
contact.unsubscribedA contact unsubscribed from a list.
contact.updatedA contact's fields, tags or subscriptions changed. Optional filters.
contact_tag_addedA tag was added to a contact. tag (name) or tag_id.
contact_tag_removedA tag was removed from a contact. tag (name) or tag_id.
conversation_assignedA conversation's assignee changed. assignee_scope, user_ids, inbox_ids, assignment_sources.
conversation_closedA conversation was closed. Optional channel_ids, tag_filter.
conversation_openedA conversation was opened. Optional channel_ids, tag_filter.
incoming_messageEvery inbound message. Optional channel_ids, tag_filter {any_of: [...]}, regex.
keyword_matchInbound message matching keywords[] with match_mode contains / exact / regex, optional channel_ids.
manualStarted from the app or POST /flows/{flow_id}/runs. No config.
scheduleTime based. schedule_kind: birthday / recurring_cron (with time_of_day_local / recurrence_rule).
whatsapp_flow.completedA contact submitted a WhatsApp Flow form.

Any event trigger may also carry a business-hours gate config.schedule with mode inside / outside and weekly windows keyed by fri / mon / sat / sun / thu / tue / wed.

IDs from your workspace​

Channel, team member, inbox, list, tag, bot, template and flow ids must belong to the workspace that owns the flow. An id from another workspace (or one that was deleted) is reported as a validation problem on save and blocks publishing. Tags in add_tags, remove_tags and tag rules are names, so they work across workspaces.

Validation errors​

When a definition is invalid, POST /flows, PATCH /flows/{flow_id} and POST /flows/{flow_id}/publish return HTTP 422:

{
"detail": {
"code": "flow_definition_invalid",
"message": "The flow has 1 problem: nodes[route].cases[0].rules[0].field 'foo.bar' uses unknown namespace 'foo'. ...",
"errors": [
{
"path": "nodes[route].cases[0].rules[0].field",
"message": "nodes[route].cases[0].rules[0].field 'foo.bar' uses unknown namespace 'foo'. ..."
}
]
}
}

errors[].path names the node by its id. Graph-level problems use an empty path or an edges[N], trigger or entry_node_id path.

A draft saves as long as its structure is valid. A published or paused flow is live: a PATCH that changes its definition or default_channel_chain must pass every publish check, otherwise it is refused with reason: "live_flow_invalid" and nothing is saved. To keep such an edit, save it as a draft copy with POST /api/v1/flows/{flow_id}/duplicate and the body {"definition": ...}.

Keep node ids stable when you edit a live flow: contacts waiting on a step whose id is no longer in the saved definition leave the flow (their run is canceled with flow_redefined). Preview how many with POST /api/v1/flows/{flow_id}/edit-impact and the body {"definition": ...}.

Complete example​

{
"schema_version": 1,
"trigger": {
"type": "keyword_match",
"config": {
"keywords": [
"pricing"
],
"match_mode": "contains"
}
},
"entry_node_id": "welcome",
"nodes": [
{
"id": "welcome",
"type": "send",
"display_name": "Welcome",
"config": {
"messages": [
{
"type": "text",
"order": 0,
"delay_seconds": 0,
"text": "Hi {{contact.first_name}}, happy to help with pricing."
}
]
}
},
{
"id": "ask_size",
"type": "choice",
"display_name": "Ask team size",
"config": {
"prompt": {
"messages": [
{
"type": "text",
"order": 0,
"delay_seconds": 0,
"text": "How big is your team?"
}
]
},
"options": [
{
"id": "small",
"label": "1 to 10"
},
{
"id": "large",
"label": "More than 10"
}
],
"attempt_timeout_seconds": 86400
}
},
{
"id": "route",
"type": "branch",
"display_name": "Route by tag",
"config": {
"cases": [
{
"label": "VIP",
"rules_op": "all",
"rules": [
{
"field": "contact.tag",
"operator": "has_tag",
"value": "VIP"
}
]
}
]
}
},
{
"id": "has_email",
"type": "condition",
"display_name": "Has email?",
"config": {
"rules": [
{
"field": "contact.email",
"operator": "is_not_empty"
}
]
}
},
{
"id": "tag_lead",
"type": "tag",
"display_name": "Tag as lead",
"config": {
"add_tags": [
"Lead"
]
}
},
{
"id": "handover",
"type": "open_conversation",
"display_name": "Hand over to sales",
"config": {
"assign_strategy": "none",
"add_conversation_tags": [
"Sales"
]
}
},
{
"id": "small_reply",
"type": "send",
"display_name": "Self-serve answer",
"config": {
"messages": [
{
"type": "text",
"order": 0,
"delay_seconds": 0,
"text": "Our Basic plan fits most small teams."
}
]
}
},
{
"id": "done",
"type": "end_flow",
"display_name": "Done",
"config": {}
}
],
"edges": [
{
"from": "welcome",
"to": "ask_size",
"branch": "default"
},
{
"from": "ask_size",
"to": "small_reply",
"branch": "small"
},
{
"from": "ask_size",
"to": "route",
"branch": "large"
},
{
"from": "ask_size",
"to": "done",
"branch": "fallback"
},
{
"from": "ask_size",
"to": "done",
"branch": "timeout"
},
{
"from": "route",
"to": "handover",
"branch": "case:0"
},
{
"from": "route",
"to": "has_email",
"branch": "else"
},
{
"from": "has_email",
"to": "tag_lead",
"branch": "yes"
},
{
"from": "has_email",
"to": "handover",
"branch": "no"
},
{
"from": "tag_lead",
"to": "handover",
"branch": "default"
},
{
"from": "small_reply",
"to": "done",
"branch": "default"
},
{
"from": "handover",
"to": "done",
"branch": "default"
}
]
}

Prompt for your AI​

Paste this together with the page above into your AI assistant, then describe the flow you want.

You write SendSeven flow definitions as JSON.
Follow the SendSeven flow format spec exactly:
- Output ONE JSON object with keys schema_version (1), trigger, entry_node_id, nodes, edges.
- Every node is {"id", "type", "config"} plus an optional "display_name" (unique).
"position" ({"x", "y"}) is optional; you may omit it.
- Use only the node types, fields and branch labels listed in the spec. Never invent keys.
- entry_node_id is the first action node (never the trigger).
- Every edge is {"from", "to", "branch"}. Use the exact branch labels of the source node.
- Variables are {{namespace.field}}, e.g. {{contact.first_name}} or {{vars.order.total}}.
- Never invent ids for channels, team members, inboxes, lists, bots, templates or flows.
Leave the field out (or ask me) unless I gave you a real id.
- Tags in add_tags, remove_tags and has_tag rules are tag NAMES, not ids.
- Put API credentials in {{secrets.NAME}}, never inline.
Return only the JSON, no commentary.