Guides
Workflows
A workflow is the node graph a caller actually experiences — built visually, versioned immutably, and executed by the agent workers on every call and session.
Workflows vs. agents#
The two are easy to conflate, so here is the split:
| Agent | Workflow | |
|---|---|---|
| What it is | A reusable conversational configuration: system prompt, LLM provider and model, STT language, TTS voice, tools, knowledge bases. | A directed graph of nodes that scripts the whole interaction from start to end. |
| Where it runs | Inside a workflow, on an Agent node (or as the target of a Handoff). | Attached to phone numbers, outbound calls, and web sessions. Only workflows are directly reachable. |
| Versioned? | No — edits apply on the next call. | Yes — draft → validate → publish, immutable versions. |
The simplest useful workflow is start → agent → hangup: one open-ended conversation. Structure appears when you add deterministic nodes around the agent — greetings, intent routing, data lookups, transfers — so the LLM improvises only where you want it to.
Versions: draft → validate → publish#
Every workflow has exactly one draft — the mutable graph you edit in the builder or via PATCH /workflows/{id} — and zero or more published versions, which are immutable snapshots numbered 1, 2, 3, …
-
Edit the draft
Changes to the draft never affect live traffic. Test drafts from the builder's test-call panel, which runs the draft graph in an isolated session.
-
Validate
POST /workflows/{id}/validatechecks the draft against the node schemas: exactly onestartnode, required config present, enum and range constraints, edges attached to real handles, no orphan subgraphs. Errors block publishing; warnings (like a template referencing an unknown variable) don't. -
Publish
POST /workflows/{id}/publishvalidates once more, then freezes the draft as the next version and points live traffic at it. In-flight calls finish on the version they started with; new calls pick up the new version immediately.
Rollback#
Published versions are never edited or deleted, so rollback is just re-publishing an older snapshot:
curl -X POST https://api.vollo.io/api/v1/workflows/7f3a9d2e-8c1b-4f6a-b5d4-e2c9a1f7b8d0/publish \
-H "Authorization: Bearer pk_live_8f3KJd92mA4qL7Zx1RttVWpc" \
-H "Content-Type: application/json" \
-d '{ "version": 2 }'This creates a new version whose graph is a copy of version 2 — history stays linear and auditable. List history with GET /workflows/{id}/versions.
The graph document#
The builder edits — and the API accepts and returns — a JSON graph with four top-level keys:
{
"schema_version": 1,
"nodes": [
{
"id": "node_start",
"type": "start",
"name": "Start",
"config": {},
"position": { "x": 0, "y": 0 }
},
{
"id": "node_hello",
"type": "speak",
"name": "Welcome",
"config": { "text": "Hello {{customer_name}}!", "interruptible": true },
"position": { "x": 260, "y": 0 }
}
],
"edges": [
{
"id": "edge_1",
"source": "node_start",
"target": "node_hello",
"source_handle": null,
"label": null
}
],
"variables": [
{ "key": "customer_name", "type": "string", "default": null, "is_secret": false }
],
"settings": { "entry_node": "node_start", "max_duration_seconds": 1800 }
}nodes[].typeis one of the types in the node reference;configmust satisfy that type's schema.edges[].source_handlenames the output handle on the source node —nullfor single-output nodes, or a handle key liketrue,false,answered, or a branch/intent key.settings.max_duration_secondshard-stops runaway executions (default 1800).
Variables#
Variables are the workflow's shared state. Declare them under variables, write them with nodes like Listen, Set Variable, and HTTP Request, and read them anywhere via templates and expressions.
| Type | Notes |
|---|---|
string | The default. Utterance transcripts, names, free text. |
number | Compared numerically in expressions. Use Transform → number to coerce. |
boolean | true/false. |
json | Structured data — API responses, parsed LLM output. Drill in with JSON Path. |
secret | Declared with "is_secret": true. Stored encrypted, redacted in the builder, transcripts, and every API response, and never interpolated into spoken text. Referenced by secret_ref config fields such as auth_secret_key on HTTP nodes. |
Seeding at start: the variables object you pass when starting a call or minting a session overrides declared defaults, so each call can carry its own customer_name or order_id. Built-ins are always available: {{call.from}}, {{call.to}}, {{call.direction}}, {{session.channel}}, {{now}}.
Expressions#
Fields typed expression — Condition, Branch conditions, Loop exit conditions — use a small, safe grammar evaluated server-side. There is no arbitrary code execution.
| Construct | Syntax |
|---|---|
| Comparisons | == != > >= < <= |
| Boolean operators | and or not |
| Membership | in — value in a list literal |
| Grouping | Parentheses |
| Variable references | {{var}} or the bare identifier var |
| Literals | Strings ("pending"), numbers (3, 0.5), booleans (true, false) |
intent == "sales"
order_status == "pending" and attempts < 3
language in ["ar", "en"]
not (sentiment == "negative") or vip == trueAn expression that references an undeclared variable validates with a warning and evaluates against an empty value at runtime — comparisons with an empty value are simply false.
Templates#
Fields typed template — spoken text, URLs, request bodies, transfer destinations — interpolate variables with double braces:
Hello {{customer_name}}, your order {{order_id}} is ready.Rules:
{{variable_name}}is replaced with the variable's current value at the moment the node runs.- Unknown variables render as empty strings; the validator reports each one as a warning so typos surface before publish.
jsonvariables interpolate as compact JSON; use JSON Path first to speak a single field.- Secret variables never interpolate into fields that reach the caller (spoken text, announcements); use them via
secret_reffields on integration nodes instead.
Completion events#
When an execution finishes, Vollo emits workflow.completed (reached an end/hangup node) or workflow.failed (unhandled node error or max_duration_seconds exceeded) to your webhook endpoints, including the final variable values — a convenient way to collect structured results without polling.