Agent workflows
Design governed conversations on a visual canvas, compile them into a deterministic runtime contract, and test them by talking, typing, simulating, or phoning before you publish.
An agent workflow turns an open-ended voice agent into a governed conversation: a graph of typed steps with named routes, hard limits, and evidence for every turn. You build the graph visually — or describe it in plain language and let AI draft it — while OpenPhonex continuously compiles it into a versioned, server-validated runtime contract. The model phrases nothing on its own authority: it chooses between the routes you declared, and the server applies the transition.
The canvas
Open any agent and choose Workflow. If the agent has no workflow yet you can start from a template (Payment reminder, Customer support, Receptionist), a blank canvas, or a natural-language brief that AI turns into an editable first draft. Creating a workflow creates only a private draft — no number is attached, no tool authorized, no call placed, nothing published.
Each step is one governed unit:
| Step | What it does |
|---|---|
| Ask | Speaks a question and waits for the caller. |
| Tell / Say | Speaks approved information; may continue silently or end the turn. |
| Read tool | Reads approved data from an attached read-only tool. Runs silently inside a turn. |
| Branch | Chooses exactly one named route from the caller's answer. Cannot invent routes. |
| Outcome | Records a structured result or review candidate. |
| Transfer | Hands the live call to a configured destination, with a declared fallback. |
| End | Speaks the farewell and closes the session. |
Routes carry the labels you write ("Can pay", "Dispute"), and those labels are what the model reasons over; the compiled contract keeps stable ids underneath so renaming a label never changes execution identity. Moving nodes, zooming, or collapsing groups changes layout metadata only — the compiled contract is proven independent of everything visual.
Global interrupts — wrong party, dispute, hardship, cease contact, callback, human request, language change — are configured once on a separate rail. They can preempt any ordinary step, and when a caller's utterance matches more than one, the server picks the highest-precedence interrupt from a fixed order; the model never decides precedence.
Every semantic edit is also available from the form/list editor without drag-and-drop, and edits autosave to the draft only. If someone else edits the same draft, stale writes are refused with the current state rather than silently overwritten — saving, publishing, and restoring all carry the revision you reviewed.
Payment reminder identity clarification
Both Payment reminder starter templates ask whether the caller is the account holder before reading or speaking account details. When a delivered reply is classified as Unclear, the workflow asks at most two clarification questions. This route covers hesitation, fragments, and empty replies rather than treating them as denial. If identity is still unconfirmed, it records Identity unconfirmed and ends without a case read or a wrong-party outcome.
Wrong party requires explicit denial of being the intended person or a clear wrong-person or wrong-number statement; missing confirmation alone is not denial. Explicit wrong-party and cease-contact replies both end the call immediately, but they are two different outcomes and the starter templates keep them apart. A wrong party stops this call and leaves the number exactly where it was. A cease contact stops this call and adds the number to your do-not-call register, which is honoured on every outbound path from then on. Each outcome's label says which of the two it did, so an exported row or a review-inbox entry states the consequence rather than only the category.
Because the consequences differ, a workflow may not point both interrupts at one outcome step, and nothing other than cease contact may reach the outcome cease contact lands on. Validation refuses either draft. The second rule is what keeps each label honest: the register entry is written by the cease-contact route itself, not by arriving at that step, so a route that reached it another way would show "added to the do-not-call register" on a call that registered nothing. Both outcomes are still review candidates rather than automatic mutation commands: the register entry is the one thing a cease contact writes on its own. Global interrupt precedence is unchanged.
Any draft whose wrong-party and cease-contact interrupts point at one existing
step was updated for you when this shipped -- drafts built from an earlier
starter template, and drafts where you wired that shared outcome yourself. The
cease-contact interrupt now lands on an outcome step of its own, with the id
cease_contact_stop (suffixed if your draft already uses that id), and the step
it used to share keeps its id and every other route that reached it, so nothing
you had wired is broken. A label you wrote yourself is kept as it is; only the
starter template's own wording was replaced. Versions you had already published
are immutable and were not touched -- the updated draft takes effect the next
time you publish. Without this your existing draft could not have been published
again, because the validation rules above refuse the shape it was in.
If you send base_revision, re-read the draft first. The repair is a write,
so it advances the draft's revision. A client holding a revision from before
the release -- an integrator that re-seeds a workflow on a schedule, for
instance -- gets 409 on its first PATCH /v1/agents/{id}/workflow or publish
after it. That is the ordinary someone-else-changed-this answer and the 409
body carries the current draft under current, so reconciling against it is
enough; nothing is lost. GET /v1/agents/{id}/workflow returns the new revision
and the split draft together.
These routing instructions depend on the decision model interpreting the reply correctly. An incorrect global interrupt decision still preempts local clarification; synthetic tests do not prove speech-recognition or model accuracy. This does not add a no-speech timer: silence that produces no transcript turn does not advance the clarification steps.
Template updates do not rewrite existing drafts, custom workflows, or immutable published versions. To adopt the clarification steps on an existing agent, explicitly update its draft, test the new routes, and publish a new version.
Validation, then testing, then publishing
Reconcile before an edit or publish
An API client reads the current state with
GET /v1/agents/{id}/workflow?version_limit=20&version_offset=0 before it
PATCHes or publishes. The agents:read response includes the editable
draft/revision, validation, safe canvas layout, active published-version
metadata, and a bounded newest-first version summary so the client can compare
what it intended to change. It is a pure read: no AI or provider call, compile,
repair, lazy creation, or other mutation occurs.
workflow_state: "not_created" is the explicit response for an existing agent
that has no draft. Create a workflow only with the write endpoint; the read
will never synthesize one.
Validation blocks testing and publishing on structural errors: dangling or type-invalid targets, unreachable required steps, cycles with no exit, missing interrupt targets, a wrong-party and a cease-contact interrupt sharing one outcome step, anything other than cease contact reaching the cease-contact outcome, an outcome label from a starter template claiming the opposite of what the route landing on it does, tool or language incompatibilities, blank spoken text, missing transfer destinations, and disclosure of account details before the right-party verification you declared succeeds. Warnings are separate and must be explicitly acknowledged before publishing.
Four test modes run against the exact draft — its voice stack, knowledge, tools, and workflow:
- Talk — a real-time microphone conversation in the browser, with the active step and traversed routes highlighted live. Microphone permission is requested only after you start.
- Chat — the same draft as text, with no microphone or voice cost. The reply you see is the step text you authored; the decision model in use is always reported.
- Simulations — repeatable synthetic personas with expected outcomes, controlled tool mocks, pass rates, and step traces grouped by conversational turn.
- Phone — a controlled test call on an attached, eligible number. You arm a short-lived pin for one declared caller number and one attached number; while it is armed, calls from that caller run the pinned draft, and every other caller continues to reach the published workflow. Pins expire on a timer and after a bounded number of matches.
Draft tests can never execute unapproved mutation tools and never publish anything implicitly. Runs record the exact pinned contract, each step, the chosen route, tool status, and the policy reason for every transition — no transcript secrets, no raw provider errors.
Publishing creates an immutable version. Ordinary callers move to it only when you publish; drafts never affect live traffic. Restoring an old version copies it into a new draft — history is never rewritten — and version history shows what changed between versions.
Serving a read tool from the call's context
A workflow's read tool declares a projection: the exact field names it may
read. On a phone call — an outbound call your application placed, or a call on a
phone pin — those fields are served from that call's own call_context, the
small non-secret object you supplied when you created the call. Nothing else is
consulted: no other call, no live backend read, no network request.
Send the projected fields under case_snapshot, or as top-level call_context
keys:
{
"call_context": {
"preferred_language": "si",
"case_snapshot": {
"balance_amount": "45300.00",
"currency": "LKR",
"due_date": "2026-08-15",
"account_reference": "FC-99182"
}
}
}case_snapshot is looked up first, per field; a field absent from it falls back
to the top-level key of the same name. Values must be strings or numbers — or a
list of flat objects of them, see Several obligations on one
account — and call_context's ordinary
limits apply (at most 32 properties per object, 512 characters per string,
4096 bytes in total).
The read is all or nothing. It succeeds only when every projected field is
present and non-empty. If one is missing, blank, or the wrong shape, the read
fails and the workflow takes that step's error route — the case-unavailable
outcome in the Payment reminder template — instead of disclosing a partial case.
The run's step evidence records the reason (case_snapshot_missing or
case_snapshot_invalid) without ever recording a value.
Saying an approved figure out loud
Spoken text is always the text you authored, word for word. To state a figure you have approved, put the projected field name in double braces in the step's own prompt:
The latest statement shows an outstanding balance of {{balance_amount}} {{currency}},
due on {{due_date}}. Are you able to take care of it today?On a phone call the braces are replaced with the values the read approved. This happens only on a step you flagged discloses debt, which validation already keeps unreachable until the caller has passed your verification step — so an approved figure can never be spoken to someone who has not been verified. A placeholder with no approved value behind it is removed rather than read aloud, and a placeholder naming a field outside the projection is removed too.
Chat, the Simulations Lab and the browser Talk test are workflow-logic tests, not
calls: they keep serving a clearly synthetic test-<field> fixture and show the
authored text with its placeholders intact.
Several obligations on one account
A projected field may be a list of flat objects — one account, several
loans. In call_context (or a campaign contact's variables) it looks like
this:
{
"obligations": [
{"reference_id": "LN-1", "overdue_amount": "45300.00", "currency": "LKR", "due_date": "2026-08-01"},
{"reference_id": "LN-2", "overdue_amount": "1200.00", "currency": "LKR", "due_date": "2026-08-28"}
]
}A list is spoken only through a section the author writes in the disclosing
step's own prompt: the body between {{#obligations}} and {{/obligations}} is
repeated once per item, in order, joined by nothing — the separator between
items is whatever the body ends with, so every word and every punctuation mark a
caller hears is still yours:
Our records show the following on your account. {{#obligations}}For reference
{{reference_id}}, an overdue amount of {{overdue_amount}} {{currency}}, due on
{{due_date}}. {{/obligations}}Are you able to take care of this today?Inside the body a placeholder resolves from the item first, then from the
read's other approved fields (a {{currency}} approved at the top level fills
in for an item without one). A value blank in one item is removed for that item
alone, with the spacing that led into it. The rules that keep a partial case off
the line still hold: the list is approved whole or not at all (an item that is
not an object, a nested list or object, true/false, a value over 512
characters, an item with nothing usable, more than 16 items or 16 keys per
item refuses the read as case_snapshot_invalid; an empty list is
case_snapshot_missing); a field a disclosing step iterates must arrive as a
list, or the read is refused rather than spoken as an empty statement; and the
approved list is kept for the run's later turns exactly as it was approved. A
section naming anything but an approved list, a bare {{obligations}}
placeholder naming a list, and any unmatched {{#…}} or {{/…}} marker are
removed, never read out.
The Payment reminder from contact variables starter template is built on
this: its one read tool (read_contact_variables) projects obligations, its
disclosing step is the section above, and on a campaign call the context is the
contact's own variables — no customer endpoint. Chat and the Talk test show the
section as authored.
Runtime guarantees
The compiled contract enforces, server-side, on every turn:
- only the active step, its allowed routes, and approved context reach the model;
- the model's structured decision is validated before anything executes;
- only the declared read-only tool for that step may run;
- per-step visit counts, a total turn budget, and a per-turn timeout are hard limits;
- unknown routes, malformed decisions, missing capabilities, and exhausted budgets fail closed with a spoken, localized farewell rather than an open line.
The workflow API — drafts, publishing, versions, runs, simulations, and phone pins — is part of the REST reference.
Delivery profiles
Choose how an agent's voice performs its speech, hear exactly that in the voice preview, and know which providers honour it.
Simulations Lab
Author realistic caller scenarios, replay them against one pinned agent version, and read hard verdicts with the exact snapshot each attempt exercised.