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:

StepWhat it does
AskSpeaks a question and waits for the caller.
Tell / SaySpeaks approved information; may continue silently or end the turn.
Read toolReads approved data from an attached read-only tool. Runs silently inside a turn.
BranchChooses exactly one named route from the caller's answer. Cannot invent routes.
OutcomeRecords a structured result or review candidate.
TransferHands the live call to a configured destination, with a declared fallback.
EndSpeaks 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.

On this page