FLOW: section adds structured execution steps to any agent. It defines a step-by-step execution graph where each step declares actions (collect information, call tools, respond, branch) and transitions to other steps.
Agents operate in reasoning mode by default, where the LLM autonomously decides actions based on the goal. Adding a FLOW: section gives an agent a structured step graph, with each step declaring whether it uses LLM reasoning or deterministic execution via the REASONING: toggle.
Flow structure
Basic syntax
Entry point
Theentry_point: property declares which step the flow begins at:
If omitted, execution starts at the first step listed in the
steps array.
Step list
Thesteps: array declares the ordered list of step names. While steps can transition to any other step (not just the next in order), the list establishes the canonical ordering:
Step definitions
Each step is defined as a named block underFLOW: with its properties indented:
Per-step REASONING toggle
Every step in aFLOW: section must declare REASONING: true or REASONING: false. This controls whether the step uses LLM reasoning or deterministic execution.
Reasoning step properties
WhenREASONING: true, the following additional properties are available:
Validation rules:
- A step with
REASONING: truemust have either a step-levelGOALor an agent-levelGOAL:defined. - A step with
REASONING: falseshould not have aGOAL(it has no effect on deterministic steps). - A step with
REASONING: falseshould not haveAVAILABLE_TOOLS(useCALLto invoke tools deterministically).
Entry guards
TheWHEN: property on a step defines a condition that must be true for the step to execute. If the condition is false, the step is skipped:
Attempt limiting
Steps can limit the number of times they execute (useful for retry loops):Step actions
SAY / RESPOND
TheRESPOND: action sends a message to the user. It supports template interpolation with {{variable}} syntax:
MODEL_CONTEXT on a response
An optionalMODEL_CONTEXT sits beside a FLOW step’s RESPOND (or a COMPLETE condition’s RESPOND) and replaces that response only in future model context — it never changes what’s actually sent to the user, the stored transcript, or session values:
MODEL_CONTEXT lets the model keep a short textual summary of what the customer saw instead. MODEL_CONTEXT is a text scalar; a non-text value fails validation (MODEL_CONTEXT_INVALID). An explicitly empty value omits the response from later model requests entirely. Omit the property to keep the historical full-response behavior. See also MODEL_CONTEXT on a tool result for the equivalent policy on a tool’s own result.
A successfully rendered, non-empty
MODEL_CONTEXT view is preserved exactly through later history compaction, summarization, and automatic conversation windowing — it’s never auto-shortened, replaced with a summary, or dropped for being old. It’s evaluated once, when the response is produced; later turns reuse that same rendered text rather than re-evaluating the template against newer data. This doesn’t raise the model’s context window or bypass the usual budget: an authored view may exceed the ordinary per-value/aggregate soft budget, but must still fit the aggregate hard capacity, or the runtime reports AUTHORED_MODEL_CONTEXT_CAPACITY_EXCEEDED rather than silently shrinking it. Keep MODEL_CONTEXT to the identifiers, status, and decision facts a later turn actually needs, and fetch further detail through a read tool — a large preserved view repeats in every later prompt, which adds up in prompt size, model cost, and latency. (This guarantee needs a matching Runtime version on every process handling the session; a rollback or a mixed-version deployment can silently lose it.)PRESENT
ThePRESENT: action displays a formatted presentation before collection. It is used alongside GATHER: to show the user what has been collected so far:
GATHER in flow steps
Within a flow step,GATHER: uses a list syntax different from the top-level GATHER: section:
Map form (alternate syntax)
Fields can also be written as a map, without the leading-. Each lower-case name one level under GATHER: starts a field, with the same properties, inline required/optional/default= values, and field-level VOICE:/ASR:/DTMF: blocks as the list form:
PROMPT, STRATEGY, and other recognized GATHER-block keywords) stay GATHER block properties, not fields. Only a lower-case (or mixed-case) name that is not a recognized GATHER-block keyword is read as a new field header.
Multi-field collection
Collect multiple fields in a single step using theFIELDS: sub-block:
Flow gather field properties
Each field in a flowGATHER: supports these properties:
Flow gather fields also accept the same additional properties as top-level
GATHER fields:
entity_ref, options, message_key,
satisfied_by, validation_process, retry_prompt, max_retries, preferences, sensitive,
sensitive_display, mask_config, pii_type, transient, extraction_pattern,
extraction_group, and semantics. In addition, a flow gather field may carry rich_content
format variants for its prompt. Note that flow gather uses validation: where the top-level
section uses validate:.Field-level listening settings (ASR:)
An ASR: block — a sibling of VOICE: on a single field — sets how the bot listens while that one field is being asked on a KoreVG voice call. Once the field is no longer being asked, the call’s normal listening settings return automatically:
Rules:
- Keys are strict — an unknown key is a compile error (
GATHER_ASR_UNKNOWN_KEY). Credentials and service IDs never go in ABL. - At most 500 vocabulary entries per field across every language, each at most 100 characters.
- One listening-settings field per step.
ASR:applies only when its field is asked on its own; a field sharing its step with other required fields gets warningGATHER_ASR_SHARED_STEPand uses the normal settings when asked together. - FLOW steps only.
ASR:on a top-level (reasoning)GATHERfield is preserved but has no effect, with compile warningGATHER_ASR_REASONING_UNSUPPORTED. - Deploy checks. A
providerthat no voice channel bound to the target environment supports blocks create/promote (GATHER_ASR_PROVIDER_UNRESOLVED); restore/rollback warn instead. An unsupported channel (non-KoreVG voice, realtime speech-to-speech), an unsupported language, too many words for the engine, a no-input wait with no-input off, or an uncovered language list are deploy warnings. - At runtime, anything that can’t be applied (an unknown engine, a rejected setting, a gateway that doesn’t confirm the change) falls back silently to the call’s normal listening settings — the call continues.
- There’s no platform switch for this feature; to stop using it, remove the
ASR:block and redeploy.
Session-value vocabulary (vocabulary.terms binding)
vocabulary.terms can read its words from a session value set earlier in the call, so the recognizer hears the specific names, streets, or codes a tool returned for this caller. Write one language’s list (or the whole list) as a single quoted "{{path}}" reference:
- The value is read each time the field is asked (first prompt, retries, rechecks). A list that changes between turns is used as it is at that moment. It applies only while that field is asked.
- The reference must be exactly one quoted
"{{path}}": dotted names with optional[n]indexes, at most 8 parts and 128 characters. Surrounding text, two references, filters, an unquoted{{...}}, or a reference inside a list are all compile errors (GATHER_ASR_TERMS_BINDING_INVALID). - Some values can never be bound (
GATHER_ASR_TERMS_BINDING_FORBIDDEN): session identity and runtime values (session,config,env,history,messages,tenant_id,project_id,user_id,session_id,session_principal_id), the protected email aliases, any root starting with_, and any path through__proto__,prototype,constructor,_secret, or_secrets. Auser.*value is personal data and needsrestore_pii: true(see below). - The value can be a string (one word) or a list of strings and
{ phrase, boost }entries. On the Deepgram keyterm path a boost has no effect. - Each word is checked when read. A word that is missing, empty, longer than 100 characters, contains a comma/semicolon/line break, duplicates an earlier word, holds a redaction placeholder or a held secret value, or holds protected personal data is left out. The question is still asked. A field never fails because of its word list.
- Words already present for a language come first. A language whose reference resolves to nothing stays empty for that question. It does NOT fall back to
default. - Limits per field per question: 2,000 entries read, 64 KiB of text read, 500 words total including literal ones. On Deepgram, a list that includes session words is held to 400 estimated tokens (literal-only lists keep 500), and the gateway accepts at most 100 keyterms.
- Only the KoreVG voice pipeline uses these words. Other channels and realtime speech-to-speech calls ignore the reference without reading the value.
Personal data in bound words (restore_pii)
Values the platform has tokenized as personal data (names, phone numbers, emails) are left out by default. To send the original value to the speech engine, BOTH of these are required:
- The field sets
vocabulary.restore_pii: true. - The project’s PII pattern for that kind of data has an explicit
speech_providerrule set to Original (PII Protection in Studio). A pattern whose default mode is Original does not count; without the rule the word is left out.
restore_pii on a field with no session-value reference is ignored with a warning.
Field-level keypad settings (DTMF:)
A DTMF: block, a sibling of ASR: under a single GATHER field in a FLOW step, sets how the bot collects a number the caller types on the phone keypad while that field is being asked on a KoreVG voice call. When the field is no longer being asked, the call’s normal keypad settings come back automatically (2 seconds between key presses, finish key #, up to 12 digits). A field without DTMF: behaves exactly as before. The caller can still answer by speaking; DTMF: changes only how typed digits are grouped into one answer:
DTMF: followed by indented digits:, finishOnKey:, interDigitTimeoutMs: lines).
Rules:
- Keys are strict: an unknown key, a malformed value, or an out-of-range value is a compile error.
minabovemaxis a compile error. ADTMF:block with no settings is a compile error. minabove 1 is a compile warning: a caller who stops early is not finished by the pause.- One keypad-settings field per step. A field with
DTMF:must be the only field in its step. Split other fields into their own steps. - FLOW steps only.
DTMF:on a top-level (reasoning) GATHER field is reported as an unknown key (W170) and has no effect. VALIDATEchecks the stored value. Atype: phonefield is stored in international format (e.g.+919876543210), so^[0-9]{10}$never matches it. Usetype: stringto check digits exactly as typed, or write the pattern for the international format.DTMF:does not change validation or retries: useVALIDATE,RETRY_PROMPT,MAX_RETRIESon the same field, plus the agent’sON_ERRORhandler forvalidation_errorwithSUBTYPE: max_retries_exceeded.DTMF:collects ordinary data. The typed number reaches the agent, its memory, and the transcript like any GATHER value. For secrets (card numbers, security codes, PINs), useSECURE_INPUTinstead.- Explicit keypad collections (
SECURE_INPUT, theivr_digit_inputtool, voice surveys) keep their own settings and are unaffected. - At runtime, settings apply with the field’s listening settings (same mechanism as
ASR:). If they can’t be applied (e.g. a realtime speech-to-speech channel, or a gateway that doesn’t confirm the change), the call keeps its normal keypad settings and continues.
GATHER block properties
AlongsideFIELDS:, a flow GATHER: block accepts these block-level properties:
The
STRATEGY: value controls the collection approach:
CORRECTIONS
WhenCORRECTIONS: true, the user can naturally correct previously collected values without restarting the step:
COMPLETE_WHEN
TheCOMPLETE_WHEN: condition specifies when the gather step is considered complete:
CALL…WITH…AS
TheCALL: action invokes a tool. Use WITH: to pass parameters and AS: to bind the result to a variable:
WITH parameters
TheWITH: block maps tool parameter names to values or variable references:
- Variable references:
amount(resolves the session variableamount) - Literal strings:
"domestic" - Expressions:
COALESCE(reference, "") - Nested objects — a key with no inline value followed by an indented sub-block becomes a nested object argument, for tools whose schema requires object parameters:
"30" → 30, "[15, 16]" → [15, 16]).
Localized WITH arguments
AWITH value can take its text from the project’s locale files (locales/<locale>/<agent>.json or locales/<locale>/_shared.json) with "{{locale.KEY}}", or a bare locale.KEY. The text is chosen for the caller’s current interaction locale (with the same fallback chain as MESSAGE_KEY — for example hi-IN, then hi), and {{…}} placeholders inside the locale text are rendered too:
CALL result is success: false with error.code: "LOCALE_KEY_MISSING", so the step’s failure branch runs and raw {{locale.KEY}} text never reaches the tool or the caller. Only authored text is treated as a locale reference; a session value that happens to contain {{locale.…}} passes through as plain data. Supported forms are "{{locale.KEY}}" (optionally with filters, like "{{locale.KEY | upper}}", and mixed with other text) and a bare locale.KEY; an expression such as locale["KEY"] or locale.KEY + "!" is not supported and the CALL is not run.
SET
TheSET: action assigns values to session variables:
variable = expression syntax. The expression is resolved at execution time and can reference:
- Literal values:
"pending",0,true,false - Variable references:
acctResult.balance - Function calls:
FORMAT_CURRENCY(amount, "USD"),COALESCE(value, "default"),ADD(a, b),SUB(a, b),ROUND(n, decimals) - Unique ID generation:
UNIQUE_ID(12) - Current timestamp:
NOW()
{{variable}} templates — the collection is parsed first, then its string values are rendered, so substituted data is never reinterpreted as JSON:
employer_name contains quotes or backslashes. Prefer a direct variable reference when constructing a computed collection instead of interpolating one into JSON-shaped text:
SET: payload_text = '[{"customer":"{{employer_name}}"}]' stores a string, not a structured value. A session TYPE declaration alone doesn’t validate every SET write; validate structured arguments at the tool boundary (pass the variable to a typed parameter, such as filters: object[], through WITH).
CHECK
TheCHECK: action evaluates a condition. If the condition is false, execution transitions to the ON_FAIL: step:
CLEAR
TheCLEAR: action removes variables from the session context:
TRANSFORM
TheTRANSFORM: action filters, maps, sorts, and limits an array from the session context:
LOG
TheLOG: action emits a trace-only diagnostic (not shown to the user). Use the inline or block form:
AWAIT_ATTACHMENT
TheAWAIT_ATTACHMENT: action pauses the step until the user uploads a file.
SECURE_INPUT
TheSECURE_INPUT: action pauses the step to collect one or more sensitive values — a card number, CVV, or similar — via DTMF and/or speech, and delivers them directly to one tool, without the raw values ever passing through the LLM, transcript, session state, logs, call recordings, or traces: speech recognition is suppressed and recording is paused while the caller enters them, and the values are wiped from memory once the delivery call returns. It’s the PCI-safe alternative to GATHER for data that must never reach the model.
AS, RESULT_FIELDS, and SUCCESS_WHEN may be written indented under the CALL line or beside it; AS: <name> may also end the CALL line itself.
Success. Without SUCCESS_WHEN, any 2xx delivery response takes ON_SUCCESS; a non-2xx status, a network failure, a timeout, or an unreadable body takes ON_FAIL. With SUCCESS_WHEN, a 2xx response must also match every listed field (compared as text, no case folding) or the step takes ON_FAIL — use this when an API can report a decline inside an HTTP 200. A delivery that timed out after the request was sent also takes ON_FAIL, even though the charge may have gone through — don’t retry payment from that step.
Reading the response. AS binds only the RESULT_FIELDS the response contains, as top-level scalar values. A field is dropped (never bound) when its value contains any collected value’s digits, contains a valid card number, or is an object or list — the step’s trace names the bound/dropped fields, never their values. The bound fields are then ordinary session values for later steps, as {{payment_result.Status}} above.
Field properties
A failed check asks for the field again, up to
MAX_ATTEMPTS. A CALL validation rule passes only when the tool returns valid: true, and may reference the field being checked plus any fields collected before it. A field accepts only PROMPT, COLLECT, and VALIDATE — anything else is ignored with a warning (W210).
References inside the delivery WITH: block must be a whole-value placeholder — pan: "{{secure.card_number}}" is valid; embedding it in a larger string (pan: "card:{{secure.card_number}}") or referencing an undeclared field is a compile error.
MESSAGE_KEY and BEHAVIOR
Two additional step-level properties:MESSAGE_KEY:— a locale catalog key used to localize the step’sRESPOND:message.BEHAVIOR:— free-form guidance text appended to the step’s reasoning goal (a convenience alias that folds intoGOAL).
RESOLVE_SIP_DESTINATION
RESOLVE_SIP_DESTINATION: is a step action that computes a dynamic SIP transfer authority (VDN/host/port) before a call_transfer step runs. It runs a feeder — a workflow tool or a session memory key — matches the candidate against the approved_authorities allow-list declared on the call_transfer tool’s sipTransferId parameter, and commits the approved authority to a trusted server-side routing channel that the transfer then reads. The model never sees or sets the destination.
The feeder’s result supplies the
{{route.vdn}} / {{route.ip}} / {{route.port}} values for the sipTransferId template. The two-zone template and the approved_authorities allow-list are declared on the tool parameter — see Dynamic destinations in the tools reference.
Branching and control flow
THEN / ON_FAIL
The most basic branching:THEN: specifies the next step on success, and ON_FAIL: specifies the step on failure:
ON_FAIL: also accepts a structured block for recovery flows — COLLECT: (fields to re-collect),
GOTO: (step), RETRY: true, RESPOND:, and THEN::
ON_RESULT
ON_RESULT: provides multi-way branching based on the result of a CALL: action. Each branch has an IF: condition, optional actions, and a THEN: transition:
ON_RESULT branch properties
The
ELSE branch (a branch with no IF condition) matches when no other condition is true.
ON_SUCCESS / ON_FAIL
An alternative toON_RESULT: for simpler success/failure branching:
ON_SUCCESS: and ON_FAIL: support conditional branches:
ON_INPUT
ON_INPUT: provides branching based on user input, typically used after a GATHER: action:
Digressions
Digressions are intent-based escapes that can interrupt the current step. They match user intent patterns and respond accordingly:Digression properties
When
RESUME: true, the user returns to the interrupted step after the digression response. When GOTO: is specified, the flow transitions to that step instead. RESUME, GOTO, and HANDOFF are terminal.
For multiple ordered actions, use the canonical
DO: block instead of flat keys. Each list item is
an action (RESPOND, SET, CLEAR, CALL, HANDOFF, DELEGATE, GOTO, RESUME), and
HANDOFF/DELEGATE may carry RETURN: and ON_RETURN: (with MAP:). Mixing flat keys with a
DO: block is discouraged.Global digressions
Digressions declared at the flow level (underglobal_digressions:) are available at every step:
Sub-intents
Sub-intents are scoped intents valid only within a specific step. They handle step-specific user requests like corrections or clarifications:Sub-intent properties
Interactive actions
Flow steps can present interactive UI elements (buttons, selects, inputs) to the user. These are attached toRESPOND: messages and handled with ON_ACTION: callbacks.
Action elements
Interactive actions are defined in anACTIONS: block within a step:
Element types
Buttons
Select (dropdown)
Input fields
Action element properties
Form submission
When a step containsinput type elements, you can configure a form submission button:
ACTIONS: block also accepts an optional RENDER_ID: to give the whole action set a stable
identifier for rendering/telemetry.
ON_ACTION callbacks
TheON_ACTION: block defines handlers for user interactions with action elements:
ON_ACTION handler properties
For multiple ordered actions in one handler, use a canonical
DO: block whose list items are
actions (SET, CLEAR, LOG, RESPOND, CALL, GOTO/TRANSITION/THEN, HANDOFF,
DELEGATE, COMPLETE).Rich content
Steps can include rich content in multiple formats alongsideRESPOND: messages. The runtime selects the appropriate format based on the channel. The block keyword is RICH_CONTENT: (canonical) or FORMATS: (original spelling, still accepted); the .agent.yaml format accepts rich_content or formats the same way.
Rich content format properties
The runtime recognizes these format keys. In addition to the messaging formats, several structured template types are available (see Rich content for their schemas):Voice configuration
Steps can include voice-specific overrides for text-to-speech engines:Per-turn endpointing
END_OF_SPEECH_TURN_TYPE is a small, deliberately-closed vocabulary describing what kind of answer the caller is expected to give next — the runtime resolves it to a concrete end-of-speech (silence/pause) timeout for the active speech recognizer, so authors don’t have to guess a millisecond value per turn:
It can be set at any VOICE scope — agent-wide under
EXECUTION.voice, on a behavior profile, on a flow step, or per RESPOND — with the narrowest scope winning, same as BARGE_IN/STREAMING:
END_OF_SPEECH_TIMEOUT_MS overrides the preset’s timeout with an explicit millisecond value when set alongside (or instead of) END_OF_SPEECH_TURN_TYPE.
Per-turn endpointing applies to the platform’s own speech-recognition pipeline. It does not affect TTS streaming, and is ignored on native realtime/speech-to-speech voice models where the provider owns turn detection. Omit it to fall back to the agent’s conversation-level pause configuration.
Scoped barge-in
barge_in (values allow / disallow) sets whether the caller can interrupt a specific spoken output — for example, forcing a compliance disclosure to finish. It may be authored at three scopes, from broadest to narrowest, with the narrower winning:
VOICE: block. Placing barge_in on the agent-wide EXECUTION.voice block, or on branch/handler voice blocks, is rejected (VOICE_BARGE_IN_ILLEGAL_PLACEMENT); an invalid value is VOICE_INVALID_BARGE_IN.
This scoped, per-output policy is distinct from the agent-wide barge-in behavior authored under
CONVERSATION → listening. Use scoped barge_in to protect (or open up) individual utterances.Scoped streaming
streaming (true / false) sets the TTS delivery mode for a specific spoken output — whether audio starts playing as soon as the first tokens are ready (streaming, the default) or the runtime waits for the complete response before speaking it (non-streaming). It follows the same authoring scopes as barge_in: step-wide, per-RESPOND, or on a named template’s VOICE: block, with the narrower scope winning.
streaming defaults to true (streaming) at every scope.
Step-level no-input (caller silence)
A FLOW step can carry its own caller-silence policy in its ownVOICE: block, as ON_NO_INPUT. It takes the same keys as CONVERSATION.listening.on_no_input (agent/project-wide) and applies on KoreVG pipeline voice while that step is the one waiting on the caller:
- Owner. When the silence timer starts, the step currently waiting owns the window. The block applies only to that step; after
COMPLETE/ESCALATE, or on a step with no block, the agent/project policy applies as usual. - Merging. Fields merge one at a time over the project and agent policy — a step can override just
timeout_msand inherit everything else.max_retries,relaxed_interval_ms,on_exhausted, andexhausted_messageall follow the same rule, andenabled: trueon a step turns silence handling on for that step even when the agent has it off. - Messages. Step messages don’t replace the agent-level list — each silence plays agent message N followed by step message N as one utterance (“Are you still there? Could you tell me your account number, Sam?”). Once the step’s own list is used up, the agent message plays alone (or the platform default, if the agent has none), so a step block never ends the call sooner than the agent alone would.
- Retry count. Restarts when the caller speaks, and when a different step (or the same step entered again) takes ownership of the window.
- Variables.
{{var}}placeholders render in agent messages, step messages, andexhausted_message. A step message with a missing variable is skipped; an agent or exhausted message with a missing variable falls back to the platform default text.
Gateway turn detection
TURN_DETECTION turns KoreVG gateway turn detection on or off for a level and sets its patience. With it on, the voice gateway decides when the caller finished speaking (voice activity, acoustic/text end-of-turn models, speech-provider finals) instead of the speech provider’s silence timer.
It can be set at any VOICE scope: a concrete RESPOND’s VOICE block, a flow-step VOICE block, a GATHER field VOICE block, a behavior-profile VOICE block, or EXECUTION.voice:
Precedence, most specific first: the response (or step, or field) that asks the caller something, the active behavior profile,
EXECUTION.voice, the voice channel’s own turn-detection setting, off. A response/step value applies to the caller’s answer to it and holds while the step re-asks (retries); the next answered response with no key falls back up the chain. On a REASONING: true step (model writes the replies), the step’s own turn_detection applies to every caller turn while the conversation stays in that step (only turn_detection is taken from that step’s VOICE, nothing else).
An invalid value is a compile error (VOICE_INVALID_TURN_DETECTION) and is never applied. A KoreVG gateway without the Dictation/Slow dictation profile uses its most patient profile instead (0.9 falls to 0.8, or 0.7 if 0.8 is also missing).
Gateway turn detection applies to KoreVG pipeline calls only, never realtime (speech-to-speech), other voice transports, or the PCI
SECURE_INPUT gather. While turn detection is on, the gateway ignores the end-of-speech timeout, so a block that turns it on AND also sets END_OF_SPEECH_TURN_TYPE or END_OF_SPEECH_TIMEOUT_MS gets a compile warning VOICE_TURN_DETECTION_WITH_END_OF_SPEECH.Step-level error handling
Steps can define local error handlers that override agent-levelON_ERROR: handlers:
default, llm_error, tool_error, routing_error, validation_error, timeout_error, and
unknown_error. A key outside this set (for example the older tool_timeout, invalid_input, or
api_error) is flagged at compile time (ON_ERROR_HANDLER_TYPE_DRIFT) and never matches at
runtime — see Error types for the
full table and the deprecated-name mapping. Narrow tool_error with SUBTYPE/SUBTYPES (known
values: rate_limit, auth_failure, network_error, tool_timeout).
Step ON_ERROR handler properties
Complete example
Related articles:
- Language overview — syntax rules and auto-detection
- Tools — tool definitions used by
CALLactions - GATHER — top-level gather field definitions
- Agent declaration — GOAL used by
REASONING: truesteps,max_flow_iterationsand model settings