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 Skysay continuously compiles it into a versioned, server-validated runtime contract. The model decides nothing on its own authority: it chooses between the routes you declared, and the server applies the transition. On the Cascade engine every line is spoken exactly as you wrote it; on GPT-Live 1 the model says each line in its own words (see Spoken workflow calls).

Voice and turn-taking

Choose the caller's speech recognizer under Voice → Transcriber. Choose interruption sensitivity under Tuning → Turn-taking. Expand Advanced overrides only when you need explicit silence or interruption durations; clearing a value restores the preset.

For an agent with a workflow, Voice also shows the published ordinary-call input per language. Calls without a bound workflow keep that published input; a workflow-only listener is never presented as the ordinary cascade input. Save settings stages the workflow input, then workflow Publish applies it.

These are the agent's shared editable settings. A workflow's Publish view shows the saved listener, its languages, credential source, and resolved timing, with links to those controls. Save pending Voice/Tuning edits before publishing the workflow. Publishing freezes a copy: changing the shared settings later does not change an existing workflow version or an in-progress call. Restoring a workflow version restores its graph; it keeps the agent's saved Voice/Tuning settings for the next publication.

A text-only workflow can have voice input turned off. A spoken workflow needs a listener that accepts every language required by the workflow and agent: a model is accepted for every language Skysay lists for it, and its Caller languages picker offers those languages. See Which voices and listeners a spoken workflow accepts.

ListenerAccepted for spoken workflowsTested by SkysayCredentials
Deepgram Nova-3All the languages it listsEnglish, TamilSkysay or your supported credential
Azure RealtimeAll the languages it listsEnglish, SinhalaSkysay only
Soniox STT-RT v5All the languages it listsEnglishSkysay or your supported credential
OpenAI GPT-Live TranscribeEnglishEnglishSkysay only

GPT-Live Transcribe is a finite workflow listener. It is not offered as a transcriber for an ordinary cascade conversation. It uses OpenAI's transcription connection directly, and bills provider-reported audio seconds, including the provider's rounding. It does not provide detected-language evidence for Follow the caller. It accepts English only, and makes no claim about automatic language switching.

For ordinary GPT-Live or Gemini Live conversations, the live engine handles listening and turn-taking. A GPT-Live spoken workflow instead uses the listener under Voice → Transcriber to hear the caller and the live session to speak. GPT-Live's workflow output is accepted in every language, with English marked Tested by Skysay; the model speaks in its own wording. Cascade workflow outputs use the agent's selected voice. See conversation engines.

Workflow presets use voice activity detection, rather than a semantic turn model. Silence holds accept 300–3000 ms and minimum interruption speech accepts 0–1000 ms. Ordinary cascade overrides retain their existing 100–2000 ms and 80–2000 ms bounds. The settings express the same intent across modes; their underlying turn detectors differ.

Configure shared input through the API

Read GET /v1/agents/{agent_id}/voice-stack and retain voice_setup.revision. Once the agent has a workflow, stage an English listener:

{
  "expected_voice_setup_revision": 0,
  "voice_setup": {
    "listener": {
      "registry_name": "openai_transcribe",
      "model": "gpt-live-transcribe",
      "languages": ["en"],
      "credential_source": "platform_managed"
    },
    "turn_taking": {
      "interruption_sensitivity": "balanced"
    }
  }
}

Send this to PATCH /v1/agents/{agent_id}/voice-stack, replacing 0 with the revision you read. A stale revision returns 409; reload and review the other edit before trying again. Do not combine voice_setup with transcriber or the overlapping turn_taking fields in one request. Existing component and tuning commands update the same owner and retain ordinary agent publication behavior.

Read the workflow draft before publishing. Send its revision as base_revision and its voice_setup_revision as expected_voice_setup_revision to POST /v1/agents/{agent_id}/workflow/publish. A voice change while publication is preparing returns 409. The workflow read contains a derived voice_input for inspection; omit that field from graph PATCH requests. Writing it is refused with instructions to use the shared voice-stack command.

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, Appointment booking), 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 toolRuns, silently inside a turn, one of the AI agent's own tools (see Tools on a workflow call), or checks that the call carries the data the next lines need. Takes its error route when it cannot.
BranchChooses exactly one named route from the caller's answer. Cannot invent routes.
OutcomeRecords a structured result or review candidate.
ActionPrepares one configured write, asks for confirmation, and follows its success, declined, change, error or unknown route. SMS and call transfer use this step.
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.

A reply to an Ask step's question, or to a spoken line that leads straight to a branch, is decided once, against that step's or branch's routes and the global interrupts together. The dispute, hardship, callback and human-request interrupts are weighed against your routes as one choice: a route you wrote for the reply ("Already paid", "Has a question", "Wants to leave a message") is taken when it fits what the caller said better, so the step you laid out answers them, and when none of your routes fits, the interrupt applies as before. Wrong party, cease contact and language change are always checked on their own and still preempt any route. When a read tool sits between the line and the branch, the branch decides after that step, separately, as before.

On an Ask step the caller still gets what they ask for alongside an answer: "Yes, it's me, but I want to speak to a person" takes the human-request interrupt rather than confirming the caller, and a request that comes with "sorry, can you repeat that?" is not dropped for a repeat. When a reply expresses several of these interrupts, such as a dispute and a hardship, all of them are reported and the fixed order above decides which one applies. This applies to an Ask step that chooses one of its routes. It does not apply to an Ask step that checks the caller's answer against a protected value, or to speech that starts before the question has been fully spoken; there the interrupts are checked on their own, as before.

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 records that the caller asked not to be contacted, for you to review. Nothing adds the number to your do-not-call register automatically -- a caller can say anything, so you decide whether a number goes on it, and the numbers you add are honoured on every outbound path. Each outcome keeps its own label, so an exported row or a review-inbox entry says which of the two happened.

The starter templates used to label the cease-contact outcome "Asked not to be contacted — call stopped, number added to the do-not-call register". Your drafts carry "Asked not to be contacted — review" instead, and so does a draft you restore from a version published with the old label, so publishing it again does not bring the old claim back. The old text is recognised however it is spaced or capitalised; a label you wrote yourself is kept as it is.

A reply to a Tell / Say line is judged together with that line as the caller heard it, including the call's values it stated, so a short "no" to a question the line asked — "Have you already paid it?" — is an answer, not a denial of identity. Only that one line is added: the workflow's other steps, the caller's earlier replies and any variable the line does not state never reach the decision model. The values are included only when the model that judges replies runs on the same provider as the agent's own model, so your data reaches no provider the call was not already using. For any other provider the line goes as you wrote it if it names no variable, and is left out if it does.

Because the two mean different things, a workflow may not point both interrupts at one outcome step, and validation refuses a draft that does. Both outcomes are review candidates rather than automatic actions: neither writes to your do-not-call register. 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. The draft comes back whole -- its variables (normalized; if any declaration cannot be read, the whole variables block is left out, and the draft's validation says why), each identity step's matches and every Action step -- so a client that reads, edits and writes the draft back keeps them. 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, tool or language incompatibilities, blank spoken text, missing transfer destinations, a line that names a variable the workflow does not declare, and a line or SMS that could say a protected variable before the caller is confirmed (PROTECTED_BEFORE_CONFIRMATION, naming the step). An incomplete or invalid listener in the agent's voice setup (INVALID_VOICE_INPUT) blocks publishing too; see Choose how the workflow hears callers. 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, filled in with your test values; the decision model in use is always reported. (An agent without a flow has a Chat mode too, driven by its instructions rather than a flow; see Test an agent.)
  • 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 workflow, and every other caller continues to reach the published workflow. A draft pin takes the draft as it is when the matching call arrives, not as it was when you armed it; if the draft changes while that call is being prepared, the call is not bound to it. Pins expire on a timer and after a bounded number of matches.

Talk and Phone are spoken calls, so they pass the same checks as a real call: funds, and a voice and listener that a spoken workflow accepts (see Spoken workflow calls). Chat and Simulations use no voice or listener.

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.

Spoken workflow calls

A phone call, a Phone test or a Talk test that runs a workflow needs two things the workflow itself does not provide: a listener that turns the caller's speech into text, and a voice that speaks the lines. Both belong to the AI agent's voice setup, not to the workflow: you choose the listener under Voice → Transcriber and the voice with the agent's other voice settings. Skysay starts a spoken workflow call only when both accept every language the call can speak or hear (see which languages count).

Choose how the workflow hears callers

The listener that hears callers is part of the AI agent's voice setup. It is configured once, under Voice → Transcriber, with its timing under Tuning → Turn-taking, and it is shared: the same saved setup serves the agent's ordinary calls and its workflow. A workflow does not have a listener of its own to edit, and nothing on the canvas or in the graph sets one.

SettingWhereWhat it does
Receive caller speechVoice → TranscriberTurns on the listener for the agent's spoken workflows. Turned off, the publish view shows No voice input (text only): the workflow runs in Chat and Simulations but cannot make spoken calls.
Speech recognition modelVoice → TranscriberThe listener, a model from the table in Voice and turn-taking.
Caller languagesVoice → TranscriberEvery language the workflow speaks, and every language the agent is set up to speak, must be in this list, and the model must accept each of them. The list offers every language Skysay lists for the model; the ones Skysay has tested with that model carry a Tested by Skysay badge.
Credential sourceVoice → TranscriberSkysay managed, or Your provider key: an active speech-to-text key for the same provider, in this workspace and project.
Additional silence (ms)Tuning → Turn-takingHow long to wait after the caller stops speaking before their turn is submitted: 300 to 3000 for a workflow.
Minimum interruption speech (ms)Tuning → Turn-takingHow much caller speech it takes to cut off a line being spoken: 0 to 1000 for a workflow. Shorter noise does not interrupt; recognized words always do.

A published workflow keeps hearing callers through that setup. Publishing freezes a copy of the saved listener and timing into the version, so each version and each call in progress keeps the listener it was published with. Changing the setting later does not alter either; it takes effect for the next version you publish. To change how a workflow hears callers:

  1. Change Voice → Transcriber or Tuning → Turn-taking and choose Save settings.
  2. Open the workflow's Publish view, which shows the saved listener, its languages, credential source and timing, and publish a new version.

Publishing is blocked while the Voice or Tuning edits are unsaved, or if the setup changed elsewhere since you loaded the agent; reload the agent and review the change first. A listener that is incomplete or invalid for the workflow (INVALID_VOICE_INPUT) blocks publishing until you fix it in Voice. Restoring an older version restores its graph only; the agent's saved voice setup stays as it is.

AI never chooses the listener for you. Generating a workflow from a brief does not touch the voice setup, and an AI edit keeps the listener you chose.

Through the API, read and change the listener with the agent's voice-stack route, as described in Configure shared input through the API. The workflow draft returns a read-only voice_input that shows what a publish would freeze; a workflow request that includes it is refused. The publish operation describes the complete shape (WorkflowVoiceInput) in the API reference.

Publishing checks that the listener covers every language. Whether the listener and the voice accept the call's languages is checked again when a call starts, as described next.

Which voices and listeners a spoken workflow accepts

A listener or voice on this list is accepted for every language Skysay lists for that model: the languages its provider supports that Skysay runs calls in, shown in the Caller languages picker for a listener and in the AI agent's language settings for a voice. GPT-Live 1 publishes no per-language list, so it is accepted in every language Skysay runs calls in. Nothing else has to happen first: no test by Skysay, no review, no release. Any other provider or model, or a language the model does not support, is refused when the call starts.

Separately, Tested by Skysay marks the languages we have run end to end on a real workflow call with that listener or voice. It is shown beside those languages in the workspace and returned as tested_languages by GET /v1/agent-catalog. It is information, not a requirement: an untested language is accepted all the same, and the quality in it is the provider's. Test your own languages before you launch.

Used asProvider and modelAcceptedTested by Skysay
ListenerDeepgram Nova-3 (nova-3)All the languages it listsEnglish, Tamil
ListenerAzure AI Speech real-time transcriptionAll the languages it listsEnglish, Sinhala
ListenerSoniox Real-time v5 (stt-rt-v5)All the languages it listsEnglish
ListenerOpenAI GPT-Live Transcribe (gpt-live-transcribe)EnglishEnglish
Voice (Cascade)ElevenLabs Flash v2.5 (eleven_flash_v2_5)EnglishEnglish
Voice (Cascade)Azure AI Speech neural voicesAll the languages it listsSinhala, English
Voice (Cascade)Fish Audio S2 Pro (s2-pro)English, TamilTamil
Voice (Cascade)Soniox (tts-rt-v2)All the languages it listsEnglish
EngineGPT-Live 1, any of its voicesEvery languageEnglish

So one listener can cover a workflow in several languages: Azure real-time transcription accepts English, Sinhala and Tamil, so an English, Sinhala and Tamil payment reminder can use it alone, spoken by an Azure voice on Cascade or by GPT-Live 1. A Tamil workflow can use Deepgram Nova-3 with Fish Audio, and a GPT-Live workflow in any language can use a listener that accepts that language.

Azure, as listener or voice, and GPT-Live run on Skysay-managed credentials only. A workspace cannot add its own Azure or OpenAI key, and a call whose workflow would use a workspace key for one of them is refused before funds are reserved (source_credential_platform_only, see When a spoken workflow call is refused). Other voices, such as Cartesia and Gemini TTS, and other listeners still work on AI agents without a workflow; they are not accepted on spoken workflow calls yet. Gemini Live cannot run a spoken workflow.

Which languages count:

  • The listener must accept every language in its Caller languages, which include every language the workflow speaks and the call's language.
  • The voice, on an AI agent with a single voice, must accept the call's language, every language the workflow is published in, and every language the AI agent is set up to speak.
  • With Follow the caller, each language has its own voice, and each voice must accept its own language only. English on ElevenLabs Flash v2.5 with Sinhala on an Azure voice is accepted, for example. The listener must accept every followed language.

How the lines are spoken

  • Cascade speaks each line exactly as you wrote it, in the agent's voice. A line is split only at spaces, into at most 128 pieces of up to 160 bytes of UTF-8 text each. A single word, or a run of text with no spaces, longer than that is refused rather than cut short; this matters for languages written without spaces.
  • GPT-Live 1 speaks each line through the call's live session, in GPT's own voice and its own wording. The meaning follows your line, but the words are not guaranteed to match it. Use Cascade when a line must be said word for word, such as a legal disclosure. A line counts as spoken only once GPT-Live has played it to the caller in full, its ending included. If the model pauses partway through a line, Skysay waits up to 3 seconds for the rest and plays it; the call's transcript shows a line only after it was played. A line before the end of the workflow that GPT-Live does not finish ends the call as a workflow failure, with ended_reason workflow_line_undelivered, rather than moving on to a step the caller never heard asked. If the closing line is not finished, the call still ends as agent_completed with the outcome the workflow recorded, and the closing line is left out of the transcript. On a GPT-Live workflow call the listener saved under Voice → Transcriber hears the caller, not GPT-Live, so a listener is required there too. GPT-Live does not navigate phone menus, so a GPT-Live workflow call runs without phone menu navigation.

On both engines the workflow decides the route; GPT-Live's wording never changes which step comes next.

On both engines, words the caller says before a step's line has been heard in full are never an answer to it; the step is spoken again. It is spoken again at most twice after the caller's words: if the caller speaks over it a third time, the call ends with ended_reason workflow_line_undelivered (see When a workflow line is not delivered). A stop request, a wrong-person reply and the workflow's other global interrupts still apply on each of those turns. Background noise that cuts a line is not counted, and neither are words said before a repeat began playing: only a repeat the caller could hear start counts toward the three.

When a spoken workflow call is refused

These checks run before Skysay reserves funds for the call. An outbound call, campaign call or Talk test that fails one is not started, and a request that starts it gets 400 with the message below, or 409 with one of the codes in the second table. On a Phone test, an inbound call whose pinned workflow fails these checks is not refused: the pin is skipped and the call is answered by the AI agent's normal configuration, as if no pin were armed.

MessageWhat to change
Select a listener for this workflow and publish a new version.The published version has no listener. Choose a transcriber under Voice, save, and publish the workflow again.
The selected workflow sources are not qualified for spoken calls.The listener or the voice is not in the table above, or does not accept one of the call's languages.
This voice engine has no qualified workflow source adapter.The AI agent's engine cannot speak workflow lines.
Gemini Live is unavailable for workflow-bound callsGemini Live cannot run workflow calls. Switch the AI agent to Cascade or GPT-Live 1.
The workflow listener configuration changed. Publish a new version.The listener frozen into the published version is no longer valid. Check the transcriber under Voice, save, and publish again.
This workflow has no spoken lines for language. Publish it again before calling.The call's language has no rendered lines.
The workflow voice configuration is invalid.The AI agent's engine and voice settings disagree. Check them and publish the AI agent again.

When the call is priced, a few more checks answer 409 with a code as the error message. They also run before any funds are reserved:

CodeWhat to change
source_adapter_unqualifiedWith Follow the caller, one followed language's voice is not in the table above or does not accept that language.
source_credential_platform_onlyThe listener or voice is Azure, or the engine is GPT-Live, and the call would use a workspace provider key. These run on Skysay-managed credentials only.
source_output_profiles_invalidThe AI agent's Follow the caller language profiles do not match what the call was checked against, or one is incomplete. Check them, publish the AI agent again and retry.

Billing

When a spoken workflow call starts, Skysay places a hold that prices each source in its provider's own unit: the listener in audio seconds (Soniox in tokens), the voice in characters (Fish Audio in UTF-8 bytes, Soniox in tokens), and on GPT-Live the session in seconds, with no backend model line and no text-to-speech line (see Billing). When the call ends, it settles to the usage of each source, and any difference from the hold is released straight away. The platform and carrier lines apply as on any call.

Some sources never report their own usage. For these, Skysay charges the seconds it measured itself:

SourceWhat is measured
Listener: Deepgram Nova-3, Azure, SonioxThe caller audio Skysay sent to the listener while it was open.
Soniox voiceThe audio the voice returned for each line.
GPT-Live 1 workflow lineThe seconds of each line the caller heard.

A measured source is charged whether the line or caller turn finished, was interrupted, or failed, because the provider received that audio. Audio that was never sent, or a line nobody heard, is not charged. Soniox bills in tokens, so the measured seconds are converted at Soniox's published usage per minute: 500 audio tokens and 250 text tokens per minute for the listener, 250 text tokens and 500 audio tokens per minute for the voice, rounded down. The other voices (ElevenLabs, Azure, Fish Audio) keep charging the text that was sent.

In the call's cost breakdown, a line charged this way is marked Measured by Skysay. When every source measured nothing (for example, the listener could not connect and no line was spoken), an answered call still pays its platform and carrier lines for the time the line was open, with each source line at zero. A call that was never answered, did not complete, or was a browser test is not charged, and its whole hold is released when it ends.

Call variables

A workflow speaks the data each call carries through call variables: {{balance_amount}} in a line is replaced with that call's value, with the same markers, sections and limits as everywhere else. A workflow agent declares its variables on the workflow, not in the agent's variables config:

{
  "variables": {
    "version": 1,
    "fields": {
      "first_name": { "type": "string" },
      "balance_amount": { "type": "number", "protected": true, "requires": "confirmation" },
      "date_of_birth": { "type": "string", "format": "date:YYYY-MM-DD", "protected": true, "requires": "confirmation" }
    }
  }
}

PATCH /v1/agents/{agent_id}/workflow takes them as the draft's variables, replacing the draft's; null removes them, and a save that leaves the key out keeps them.

An agent whose publication already declares its own variables hands them to its workflow when that workflow is published: in the same step the agent's own declaration is cleared (its SMS templates, SMS mode and caller lookup stay) and the agent is republished against the workflow's. The publish response lists the cleared names the workflow declares in variables_moved_to_workflow, and the cleared names no line used, which the workflow does not declare, in variables_dropped (each an empty list when there are none). The cleared declaration is kept, word for word, in the audit event agent.variables_moved_to_workflow (GET /v1/audit-events), with the workflow_version, the moved and dropped names and the cleared_variables, so the handover can be seen and the declaration recovered. If the agent's greeting, its instructions or one of its SMS templates names a variable the workflow does not declare, the publish is refused with 400 variables_config_invalid and nothing changes -- declare it on the workflow and publish again. The issue says where: UNDECLARED_VARIABLE at the greeting or instructions, or the template check's own code at sms_templates.<id>.text.<language>. They are checked when the workflow is published, with every line: a line that names an undeclared variable, prints an optional one outside its own {{#name}} section, or has a misspelled or unbalanced marker is a validation error naming the step. In the dashboard they are on the workflow's Variables tab.

Every {{ in a line starts a marker. There is no way to write two literal braces in a line: a {{ that is not a valid marker is refused when the workflow is published.

Protected values

A variable marked protected is held back until the caller has shown they are the right person — a balance, an account number, a date of birth. Until then it is not spoken, not given to any model, not sent by SMS, and not sent to any tool. The workflow is checked when you publish it: a line or SMS the caller could reach before being confirmed that names a protected variable is refused as PROTECTED_BEFORE_CONFIRMATION, naming the step, whichever way the call reaches it — from the entry step, an interrupt or the fallback step. This holds on GPT-Live as on Cascade: the GPT-Live session that speaks the workflow is given each line only as the workflow releases it, so it is given no protected value before the confirming step either.

A protected variable's requires says what releases it (confirmation by default):

  • confirmation — the caller confirms who they are. An Ask step marked verifies_identity: true names the routes that mean "yes, that's me" in verified_routes; taking one of them confirms the caller. This is a conversational check: it stops a wrong person who says so, not one who lies.
  • knowledge_match — the caller states something only they know, and the platform compares it with a protected value the caller never heard.

For a knowledge_match, the identity step declares what to compare:

{
  "id": "check_birthday", "type": "ask", "prompt": "What is your date of birth?",
  "verifies_identity": true, "verified_routes": ["matched"],
  "matches": { "field": "date_of_birth", "kind": "date", "on_mismatch": "not_matched" },
  "routes": [
    { "id": "matched", "label": "Gave their date of birth", "target": "explain_balance" },
    { "id": "not_matched", "label": "Did not match", "target": "identity_unconfirmed" }
  ]
}
  • The decision model only extracts what the caller said; it never sees the stored value and never decides whether it matches.
  • kind is how the two are compared: number (numerically), digits (for a field with format: identifier: spaces, hyphens and dots ignored), casefold (text, ignoring case) or date (for a field with a date: format, whatever way the caller said it).
  • A match takes the step's one verified route and releases the fields. A mismatch, or an answer that cannot be read as the kind, takes on_mismatch, another of the step's own routes. When the caller gives no answer at all, the step is asked again, up to its visit limit, and then takes on_mismatch.
  • A requires: "confirmation" field is released by either method; a requires: "knowledge_match" field only by a match.

Validation refuses a matches that is not on an identity Ask step with exactly one verified route, that names a variable that is not protected, whose kind does not fit the variable, or whose on_mismatch is not another route of the same step (INVALID_MATCHES, MATCHES_WITHOUT_VERIFICATION, MATCH_VERIFIED_ROUTE_NOT_SINGLE, MATCH_FIELD_NOT_PROTECTED, MATCH_KIND_INCOMPATIBLE, MATCH_MISMATCH_ROUTE_INVALID).

The confirmation is recorded with the turn that confirmed it, and a protected value is released only once that turn is stored: a line in the same turn, after the confirming step, may say it.

Read tool steps

A Read tool step declares a projection: the variable names the lines after it rely on. On a call it checks that each of them has a value on this call — nothing is fetched, and the step never reads a value out. If one is missing, the read fails and the workflow takes the step's error route (the case-unavailable outcome in the Payment reminder templates), so a line is never spoken with part of the data missing. The run's step evidence records the reason (case_snapshot_missing), never a value.

A Read tool step can also run one of the AI agent's own tools, such as its knowledge base search or its calendar; see Tools on a workflow call.

Several obligations on one account

A variable may be a list — one account, several loans — declared with its item keys:

"obligations": {
  "type": "list", "protected": true, "requires": "confirmation",
  "item": { "reference_id": "string", "overdue_amount": "string", "currency": "string", "due_date": "string" }
}

A list is spoken only through a section: the body between {{#obligations}} and {{/obligations}} is repeated once per item, in order, with the item's own keys in scope. Every word and punctuation mark a caller hears is still yours:

Our records show the following on your account. {{#obligations}}For reference
{{#reference_id}} {{reference_id}}{{/reference_id}}, an overdue amount of
{{#overdue_amount}} {{overdue_amount}}{{/overdue_amount}}{{#currency}} {{currency}}{{/currency}}.
{{/obligations}}Are you able to take care of this today?

A list holds at most 16 items. The Payment reminder from contact variables starter template is built on this: its one read tool (read_contact_variables) checks obligations, its disclosing step is the section above, and on a campaign call the values are the contact's own variables — no customer endpoint.

Send SMS steps

Use an Action with the AI agent's send_sms tool. Set its arguments to {"body": "Your appointment is tomorrow."} or a published template invocation. Authored arguments can use call-variable templates; protected values require their identity gate first. Connect all five Action routes.

The caller hears the proposed message and recipient before being asked to confirm. Only a later explicit yes queues the send, after that turn commits. The call waits for its receipt. A provider timeout takes on_unknown and is never automatically resent. An accepted send does not prove delivery to the recipient's handset. See Confirming business actions.

Transfer steps

Use an Action with the AI agent's Transfer to a human tool. The read-back identifies its configured destination and asks the caller to approve the transfer. A later explicit yes permits one dial attempt; a no or changed request does not. The workspace's outbound policy and the tool's destination restrictions still apply.

After the dial is accepted, the workflow waits for its actual outcome. An answered call connects the caller and person, ending the workflow with status transferred. A busy line, no answer, rejection or failure takes on_error; an uncertain execution takes on_unknown. The call's transfer state in the Calls drawer records the dial outcome. While ringing, caller speech does not answer another workflow question.

A transfer says nothing of its own beyond its read-back, so put a Tell / Say step before it ("One moment, I'll put you through.") if the caller should hear one after the yes. While the person's phone rings the caller hears silence. A call with a person on it dials from your number the caller reached (inbound) or the number the call was placed from (outbound). Transfer Actions run on Cascade and GPT-Live workflows alike.

The run's trace records how the step went: tool_status success when the person answered, otherwise error with a policy_reason such as transfer:busy, transfer:no_answer, transfer:rejected, transfer:failed or transfer:caller_hangup, or write_not_configured when the AI agent has no Transfer to a human tool.

A browser Talk test has no phone number on the line and cannot dial a person. Chat and scripted simulations use recorded-only outcomes after confirmation. Use a Phone test to try a transfer. See Confirming business actions.

Testing with values

Talk and Chat render every line with the test values you give them (test_values), exactly as a call admits them, and run identity steps too: a comparison checks the caller's answer against the test value, and a missing test value counts as a mismatch. Protected values are shown only after the confirming step. Chat and scripted simulations record Action outcomes without performing external writes. In a scripted simulation, taking the identity step's verified route counts as a match, and anything else as a mismatch.

Versions from before call variables

When your workspace moves to call variables, every workflow version published before then keeps its number and shows as Archived before call variables (status archived_v2). An archived version cannot run a call (workflow_binding_failed) and cannot be restored.

Rerunning a simulation run from before the move:

  • a run against the version that was active reruns on its migrated copy, which says the same words;
  • a run against an older version is refused with 409 ("This run's workflow version predates call variables and could not be moved to them. Run the suite again.");
  • a run against a draft from before the move is refused with 409 ("This run's draft predates call variables. Run the suite again against the current draft.").

The version that was active gets a new copy, numbered after the workflow's highest version and moved to call variables. It becomes the active version and shows with its origin, for example v5 (migrated from v3); the API's version summary carries the old number as migrated_from_version (null on every other version). Your next publish takes the next number.

Restoring an archived version answers 409, naming the copy to restore instead ("Version 3 was moved to call variables as version 5. Restore that version instead."), or saying the version predates call variables and was not moved. To take calls on the current workflow, publish the draft, which declares its variables.

Tools on a workflow call

On a workflow call the AI agent's model is offered no tool of its own: the workflow decides every turn, and its Read tool steps run the AI agent's tools for it. A step can run one of these:

ToolWhat the step doesWhat a later line can say
Search the knowledge baseSearches the knowledge bases attached to the AI agent for the caller's question and quotes the documents' own sentences that answer it.{{tool.knowledge_answer}}: whole sentences of the documents, exactly as written and in the documents' language
Check calendar availabilityAsks the AI agent's calendar for free times in the range the caller asked about.{{tool.available_times}}: up to three offered times with their dates, said in the call's language with the calendar's time zone
Book an appointmentAn Action books one offered time after its built-in read-back and confirmation.Five outcome routes; the read-back includes the frozen appointment details.
Integration or webhook toolRead tools use Read tool; write tools use Action and require confirmation.Reads expose the declared {{tool.<key>}} result values. Writes follow receipt outcome routes.

A tool runs only if the AI agent has it. Nothing is attached for you: the knowledge search is there once you attach a knowledge base to the AI agent (Knowledge Bases), the calendar tools once you select it on a calendar connection and choose Enable booking (Calendar booking), and an Integration action once you add it to the AI agent's tools. If the AI agent does not have the tool when the call runs, or the tool fails, finds nothing or runs out of time, the step takes its error route.

Choose a tool for a step

Select a Read tool step and open its Tool list. The tools the AI agent has are grouped under This AI agent's tools; if there are none, the inspector says what to attach first. For an Integration action, enter its Result keys: one to eight top-level keys of the action's result, separated by commas. A line says each one as {{tool.<key>}}; a value that is not text or a number, or is missing, fails the step.

Read-tool arguments are collected from the caller. An Action also accepts authored JSON arguments with call-variable templates. The platform works them out from what the caller said: the question to search for, the time range to check, and, for a booking, the chosen time, the caller's name, and the email address and any answers the calendar's event type needs. If something is still missing, the step takes its error route, and what the caller already gave is kept for the next time the step runs.

Place a tool step where the caller has just answered, before the line that speaks its result: after an Ask, or after a Branch that follows one. A tool step reached after a spoken line of the same turn does not run and takes its error route, because every line of a turn is said with one set of values; validation warns about it (AGENT_TOOL_AFTER_SPOKEN_LINE).

Speak a result

A step's results are values in the tool. namespace that later lines of the workflow can say. They are optional, so a line says one inside its own section, as with any optional call variable:

{{#tool.knowledge_answer}}{{tool.knowledge_answer}}{{/tool.knowledge_answer}} Is there anything else I can help with?
{{#tool.available_times}}I have {{tool.available_times}}.{{/tool.available_times}} Which time would you like, and what name should I book it under?

With a calendar in Europe/Tallinn, the second line is said as "I have Tuesday, November 3, 2026, 10:00 AM or 11:30 AM or Wednesday, November 4, 2026, 2:00 PM (Eastern European Time)." In another language the dates, times and time zone follow that language's own conventions.

  • A result is never a call variable. A contact, call_context or a campaign cannot supply it, and you do not declare it on the Variables tab.
  • Only the workflow's spoken lines can say a result. Action arguments, the greeting and the instructions cannot name one; validation refuses a draft that does.
  • A step that fails clears its earlier results, so a later line never says a previous answer as if it were this one.
  • A knowledge answer is never text a model wrote. A model only picks which sentences of the retrieved passages answer the question; the platform checks that they are the document's exact words and says each whole sentence they come from. When nothing in the documents answers, the step takes its error route.
  • A result is never cut short. A knowledge answer or a list of times over 300 characters, an Integration value over 200 characters (600 for one action's values together), or a line that would grow past its limit with the result in it fails the step instead.

Book an appointment

Choose calendar_book_appointment in an Action step after checking availability. Its confirmation is built in, as for every other write. Connect next, on_declined, on_change, on_error and on_unknown. The last route is for an uncertain provider result and must not claim the booking succeeded.

The platform freezes the selected offer, caller details and calendar configuration, reads the entire booking back, and asks one yes/no question. A later unconditional yes authorizes those details. An unclear answer gets one clarification; changes need a new read-back and yes. There is no timing-based shortcut or model-supplied confirmation flag.

{
  "id": "book_appointment",
  "type": "action",
  "name": "Book appointment",
  "tool": "calendar_book_appointment",
  "next": "booked",
  "on_declined": "declined",
  "on_change": "collect_details",
  "on_error": "booking_error",
  "on_unknown": "check_booking"
}

Declare the tool with source: "agent", kind: "write" and an empty projection. Each route names a real step. Arguments omitted from the Action are collected from the caller. Calendar offer expiry, resource conflicts, provider rules and browser-test restrictions still apply. Read the full business-action contract before wiring recovery.

Starter templates with tools

  • Customer support searches the AI agent's knowledge base and says the answer, or escalates when nothing is found.
  • Appointment booking answers questions from the knowledge base, offers free times from the attached calendar, and books one after the read-back and a yes, with one clarification for an unclear answer and a new read-back for any change.

Each tool step in them runs only once the AI agent has that tool; until then it takes its error route. A template does not change a draft you already have.

Engines and tests

  • Cascade says each line with its results exactly as written. GPT-Live 1 is given each line, results filled in, to read exactly, and says it in its own voice; its words are not guaranteed to match. Gemini Live cannot run workflow calls.
  • Bookings follow the same rule on every engine, GPT-Live included, as for an AI agent without a workflow: the engine speaks the platform's read-back line, the caller's explicit answer is taken as a yes, a no, a change or unclear (with the one clarification), and only a yes books exactly the details read back. The calendar's own checks are unchanged.
  • Outbound calls, an inbound call from a caller you armed a Phone test for, and the Talk test run tool steps the same way, with the AI agent's real tools. Talk is a browser test, so its booking writes only to a calendar connection set up for test bookings; see Calendar booking.
  • Chat calls no real tool. A step whose tool the AI agent has not configured fails as it would on a call (tool_not_configured): no knowledge base with a ready document is attached, or the tool is not in the AI agent's tools. Every other tool step succeeds with clearly synthetic values (test-knowledge_answer, test-available_times), and an Action goes through confirmation with a recorded-only synthetic receipt. Scripted simulations use their authored tool and transfer outcomes after confirmation.

Which tools a step may run

The step can use only a tool configured on its own AI agent, with its actual read/write kind. A declaration cannot turn a write into a read. Reads run without business confirmation; writes use Action. Ending a call and language switching remain conversation controls. Protected values keep their existing disclosure and tool restrictions; adding an Action never bypasses them.

A workflow's tool results are kept with the call's transcript and deleted with it, when the call's recording is deleted or its retention period ends.

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 tool the active step declares may run, and the AI agent's model is offered none; every business write runs only after the caller's later explicit yes to its read-back;
  • a protected value leaves the server only after the caller's confirmation is stored, and only in the lines after it;
  • 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.