Workflow definitions
The workflow definition contract (v1): every field of nodes and edges, the validation rules, and a complete working example.
This page is the field-by-field reference for the workflow definition — the JSON graph you save via the Workflows API or build visually in the dashboard. For the concept and how workflows interact with assistants, start at Workflows.
Top level
| Field | Type | Required | Description |
|---|---|---|---|
version | integer | Yes | Contract version. Currently always 1. |
entry_node | string | Yes | Node id where every call starts. Must be a conversation node. |
global_prompt | string | No | Prepended to the instructions of every conversation node. |
nodes | array | Yes | The steps. At least one node. |
edges | array | No | The transitions between steps. |
Node ids are lowercase: letters, digits and underscores, starting with a letter (^[a-z][a-z0-9_]{0,40}$), and must be unique.
Conversation node
The talking step. The entry node is always a conversation node.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique node id |
type | string | Yes | conversation |
instructions | string | Yes | System prompt for this step (after the global_prompt) |
first_line | string | No | Fixed line spoken when the step becomes active. On realtime models it is passed as an opening hint instead of spoken verbatim. |
global | boolean | No | Reachable from every conversation node without edges |
global_description | string | If global | When a caller should be brought here |
llm_config | object | No | Per-step model override, same shape as an assistant's llm_config for the text provider you pick (pipeline only) |
voice | string | No | Voice swap within the assistant's provider (realtime: the speech-to-speech voice; pipeline: the TTS voice). Mutually exclusive with tts_config. |
tts_config | object | No | Full text-to-speech override incl. provider parameters (pipeline only) |
stt_config | object | No | Speech-recognition override, same shape as an assistant's stt_config (pipeline only) |
tools | array | No | Tools available in this step, same format as assistant tools |
ui | object | No | Canvas position (x, y). Ignored by the calling engine. |
Global nodes. A global conversation node gets an implicit transition from every other conversation node, described by its global_description. An explicit edge to the same node takes precedence. Global nodes also count as reachability roots — a graph section only reachable via a global node is valid.
Per-step models. llm_config is allowed on conversation nodes only, and requires a pipeline (non-realtime) assistant on the number. A step takes the same fields as an assistant does for that provider, because the block is handed to the same provider factory: the reference on llm_config, tts_config and stt_config lists them per provider, and the node schema names the subset that applies to a step. Leave model out and the provider's catalog default is used; leave provider out of an llm_config, tts_config or stt_config and the step is rejected before pickup instead of quietly running on the self-hosted engine. An unknown provider or model does not get that far: it is refused when you save the workflow, with a 400 naming the node and the field. Everything that depends on the assistant on the number is constructed once at call start, fail-closed, and rejects before pickup instead of failing mid-conversation. The entry node's override applies to the whole session.
Per-step voices and speech. voice works on both modes as a voice swap within the assistant's provider (on realtime assistants a new speech connection with the other voice is prepared per step). tts_config and stt_config are full replacements of the assistant's speech configs for that step — replace, not merge, so a provider switch never carries half the old provider's parameters. Both are pipeline-only and reject on realtime assistants with a developer-readable reason; the entry node's overrides apply to the whole session. All of it prebuilds fail-closed at call start.
Tool node
A background step: the platform calls the webhook itself and advances automatically.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique node id |
type | string | Yes | tool |
tool_name | string | Yes | Name sent in the tool-calls webhook |
url | string | No | Per-node URL override; defaults to the webhook resolved for the call |
arguments | object | No | Static arguments sent with the call |
first_line | string | No | Hold line spoken while the call runs |
ui | object | No | Canvas position. Ignored by the engine. |
On a pipeline assistant the hold line is spoken through text-to-speech, word for word. A realtime (speech-to-speech) assistant has no such step: there the line goes into the step's instructions, the platform opens a turn and waits for the model to speak, so the caller hears the model's rendering of your line, in the caller's language when that differs. If the model produces nothing, or the bounded wait runs out, the platform logs a warning on our side and calls the webhook anyway. Both paths date from the September 2026 release; before it, a hold line on a realtime assistant was skipped outright and the caller heard the whole webhook round trip as silence.
The result (or the error, if the call fails) is placed in the conversation context, and the flow advances along the node's single outgoing edge either way — the next step can see and react to what happened. For arguments the assistant should collect from the caller during the conversation, use a regular tool on a conversation node; tool-node arguments are static.
Transfer node
Hands the call to a human. Terminal.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique node id |
type | string | Yes | transfer |
destination | string | Yes | E.164 phone number (+31612345678) |
first_line | string | No | Announcement spoken once before dialing |
ui | object | No | Canvas position. Ignored by the engine. |
The platform speaks the announcement and starts the transfer as soon as the node becomes active — the action is executed in code and cannot be forgotten or repeated. The number needs a SIP trunk that allows transfers.
The announcement follows the same two paths as a tool node's hold line: spoken word for word through text-to-speech on a pipeline assistant, and produced by the model itself from the step's instructions on a realtime one, where the platform opens the turn and waits for it. Silence there is possible and is logged on our side as a warning; the transfer goes through either way, so a caller can end up connected without having heard the announcement. Both paths date from the September 2026 release; before it, a transfer node on a realtime assistant gave two seconds of silence and then connected the caller with no warning at all.
Transfer nodes are blind transfers. The caller is bridged the moment the destination starts ringing, so a line nobody picks up sends them to its voicemail with no assistant left to help. The monitored-transfer fields of the transfer tool — wait_for_answer, answer_timeout_seconds and fallback_destinations — do not exist on a transfer node; written there they are accepted and ignored. When you need no-answer handling, do not draw an edge to a transfer node: give a conversation node a regular transfer_call tool with those fields in its tools array. The node then keeps the caller when nobody answers, and can take a message or offer an alternative.
End node
Ends the call politely. Terminal.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique node id |
type | string | Yes | end |
first_line | string | No | Goodbye line; defaults to a neutral thank-you |
ui | object | No | Canvas position. Ignored by the engine. |
Edges
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source node id (conversation or tool nodes only) |
to | string | Yes | Target node id |
description | string | Yes* | When the assistant should take this transition. Optional on edges leaving a tool node. |
message | string | No | Line spoken during the hand-over |
Write descriptions as observable conditions — "The caller asks about opening hours" — not instructions. The assistant reads them to decide where the conversation goes next.
Validation rules
Definitions are validated when you save them (API and dashboard alike) and again, fail-closed, when a call comes in. The full rule set:
versionmust be1;nodesmust be a non-empty array.- Node ids match
^[a-z][a-z0-9_]{0,40}$and are unique. entry_nodereferences an existing conversation node.- Conversation nodes require non-empty
instructions. - Transfer nodes require a valid E.164
destination. - Tool nodes require a non-empty
tool_nameand exactly one outgoing edge. - End and transfer nodes cannot have outgoing edges.
- Edge endpoints must exist; no self-loops; no duplicate
from/topairs. - Edge
descriptionis required, except on edges leaving a tool node. globalis allowed on conversation nodes only and requires aglobal_description.llm_config,voice,tts_configandstt_configare allowed on conversation nodes only;voicemust be a non-empty string,tts_config/stt_configmust be objects, andvoiceandtts_configare mutually exclusive.- A node's own tool names must not collide with the generated transition tools (
go_to_<node id>). - Every node must be reachable from
entry_node(global nodes count as extra roots).
Violations are returned as a details array, one specific message per problem:
{
"error": "Invalid workflow definition",
"details": [
"node 'human': transfer nodes require destination in E.164 format (e.g. +31612345678)",
"node 'lookup': tool nodes require exactly one outgoing edge (got 0)"
]
}Complete example
A dental practice reception: general questions at the reception, an availability lookup as a background step, a scheduling specialist on a stronger model, a human transfer on request, and a clean goodbye. The reception is global, so "put me back to the reception" works from anywhere.
{
"version": 1,
"entry_node": "reception",
"global_prompt": "You work for Riverside Dental. Be warm and concise.",
"nodes": [
{
"id": "reception",
"type": "conversation",
"instructions": "You are the receptionist. Handle general questions about the practice. When the caller asks about availability, hand off to the availability check.",
"first_line": "Good afternoon, Riverside Dental. How can I help you?",
"global": true,
"global_description": "The caller wants the reception back or has a general question."
},
{
"id": "check_availability",
"type": "tool",
"tool_name": "check_availability",
"url": "https://api.example.com/availability",
"arguments": { "location": "riverside", "days_ahead": 7 },
"first_line": "One moment while I look that up for you."
},
{
"id": "specialist",
"type": "conversation",
"instructions": "You are Sam, the scheduling specialist. Use the availability result from the context to help the caller pick a slot.",
"first_line": "You are speaking with Sam, the scheduling specialist.",
"llm_config": { "provider": "openai", "model": "gpt-5.4-mini" }
},
{
"id": "human",
"type": "transfer",
"destination": "+31201234567",
"first_line": "I am transferring you to a colleague now, one moment please."
},
{
"id": "goodbye",
"type": "end",
"first_line": "Thanks for calling Riverside Dental. Have a great day!"
}
],
"edges": [
{
"from": "reception",
"to": "check_availability",
"description": "The caller asks about available appointment slots.",
"message": "Let me check that for you."
},
{ "from": "check_availability", "to": "specialist" },
{
"from": "specialist",
"to": "human",
"description": "The caller explicitly asks for a human employee."
},
{
"from": "reception",
"to": "goodbye",
"description": "The caller indicates the conversation is finished."
},
{
"from": "specialist",
"to": "goodbye",
"description": "The caller indicates the conversation is finished."
}
]
}Workflows
Build multi-step call flows on a visual canvas: conversation steps, background tool calls, human transfers and clean endings — with one assistant supplying the voice and call settings.
Web Calls
Let users speak directly to an AI assistant from their browser via WebRTC, no phone number required.