Workflows
A workflow is the behaviour layer attached to a flow. It splits a call across several specialised sub-agents, decides when to hand the caller to a person, and names the fields to extract from the conversation.
Attach one by setting workflow_id on a flow.
List workflows
GET /api/voice/workflows
Get a workflow
GET /api/voice/workflows/{uuid}
Create a workflow
POST /api/voice/workflows
{
"name": "Support triage",
"description": "Routes billing and delivery questions, escalates the rest",
"agents": [
{
"id": "billing",
"name": "Billing",
"handles": "invoices, refunds, payment failures, plan changes",
"prompt": "Answer billing questions using only the attached documents.",
"datasource_ids": ["kb-uuid-1"],
"follow_up_questions": ["Do you have the invoice number?"],
"inject_date": false
},
{
"id": "delivery",
"name": "Delivery",
"handles": "shipping status, delivery dates, address changes",
"prompt": "Give delivery windows. Never promise a date you cannot verify.",
"inject_date": true
}
],
"human_handoff": {
"enabled": true,
"phone": "+915550019999",
"pre_transfer_message": "Let me put you through to a colleague."
},
"call_end": {
"resolved_message": "Glad I could help.",
"unresolved_message": "I'll have someone follow up with you."
},
"data_extraction": [
{ "key": "invoice_number", "description": "The invoice the caller referenced" },
{ "key": "promised_date", "description": "Any delivery date the agent committed to" }
],
"persona": { "role": "Support specialist", "background": "Three years on the billing desk." },
"personality": { "tone": "empathetic", "traits": ["patient"], "style_notes": "Never rush a frustrated caller." },
"override_persona": true
}
Update a workflow
PATCH /api/voice/workflows/{uuid}
Delete a workflow
DELETE /api/voice/workflows/{uuid} → 204
Sub-agents
Each entry in agents is a specialist the turn router can switch to
mid-conversation.
| Field | Type | Notes |
|---|---|---|
id | string | Stable identifier, referenced in routing decisions |
name | string | Display name |
handles | string | Free text, shown to the intent classifier — prompt input, not a label |
prompt | string | Instructions used while this agent holds the turn |
datasource_ids | string[] | Knowledge base documents this agent may cite |
database_ids | string[] | Structured sources |
follow_up_questions | string[] | Suggested probes |
inject_date | bool | Prepends today's date, for date-relative answers |
handles is worth writing carefully — it is how the classifier decides which
agent should answer. "invoices, refunds, payment failures" routes far better
than "billing stuff".
Routing runs on partial transcripts while the caller is still speaking, so by the time they stop the decision is usually already made and costs the turn nothing.
Human handoff
{
"enabled": true,
"phone": "+915550019999",
"pre_transfer_message": "Let me put you through to a colleague."
}
enabled: false with a number still on file is a deliberate off switch — the
number is not used.
How a transfer runs
- The agent says something matching a handoff phrase, or the router decides one is needed
- Handoff XML is registered against the live call through the provider's API
- The media socket is closed, which frees the leg so the provider fetches that XML and dials the person
The order matters in both directions: registering without closing leaves the caller on a line that was never handed over, and closing first drops them before there is anywhere to send them.
The caller ID presented to the person is your number — the one dialled on an inbound call, the one you dialled from on an outbound one. Providers silently refuse a caller ID the account does not own, which looks like a successful transfer where nobody's phone rings.
If handoff is requested with no destination configured, the call is hung up rather than left with an agent that has stopped answering.
The destination number lives on the workflow, so a flow with no workflow
attached cannot transfer. Attach one by setting workflow_id on the flow.
Call endings
{
"resolved_message": "Glad I could help.",
"unresolved_message": "I'll have someone follow up with you."
}
Closing lines for the two cases. Note that phrases like "have a great day" inside these messages also trigger the hangup path — which here is what you want.
Data extraction
[{ "key": "invoice_number", "description": "The invoice the caller referenced" }]
Names the fields worth pulling out of a conversation. description is what the
model is told to look for, so it should read as an instruction rather than a
label.
To consume extracted data in your own systems, subscribe to the call.ended
webhook, which delivers the full transcript as each
call finishes.
Persona override
persona and personality restate the agent's identity for this workflow.
With override_persona: true they replace the agent's
own values; otherwise the agent's are kept and these are ignored.
Useful when one voice agent serves several lines that need different behaviour — the same voice and name, a different job.