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:

AgentWorkflow
What it isA 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 runsInside 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, …

  1. 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.

  2. Validate

    POST /workflows/{id}/validate checks the draft against the node schemas: exactly one start node, 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.

  3. Publish

    POST /workflows/{id}/publish validates 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 / shellRoll back to version 2
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:

JSONworkflow.graph
{
  "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 }
}

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.

TypeNotes
stringThe default. Utterance transcripts, names, free text.
numberCompared numerically in expressions. Use Transform → number to coerce.
booleantrue/false.
jsonStructured data — API responses, parsed LLM output. Drill in with JSON Path.
secretDeclared 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 expressionCondition, Branch conditions, Loop exit conditions — use a small, safe grammar evaluated server-side. There is no arbitrary code execution.

ConstructSyntax
Comparisons==!=>>=<<=
Boolean operatorsandornot
Membershipin — value in a list literal
GroupingParentheses
Variable references{{var}} or the bare identifier var
LiteralsStrings ("pending"), numbers (3, 0.5), booleans (true, false)
TextExpression examples
intent == "sales"
order_status == "pending" and attempts < 3
language in ["ar", "en"]
not (sentiment == "negative") or vip == true

An 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:

TextTemplate
Hello {{customer_name}}, your order {{order_id}} is ready.

Rules:

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.