Skip to main content

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.

FieldTypeNotes
idstringStable identifier, referenced in routing decisions
namestringDisplay name
handlesstringFree text, shown to the intent classifier — prompt input, not a label
promptstringInstructions used while this agent holds the turn
datasource_idsstring[]Knowledge base documents this agent may cite
database_idsstring[]Structured sources
follow_up_questionsstring[]Suggested probes
inject_dateboolPrepends 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​

  1. The agent says something matching a handoff phrase, or the router decides one is needed
  2. Handoff XML is registered against the live call through the provider's API
  3. 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.

Handoff requires a workflow

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.