Skip to main content
The 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

The entry_point: property declares which step the flow begins at:
If omitted, execution starts at the first step listed in the steps array.

Step list

The steps: 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 under FLOW: with its properties indented:

Step headers and repeated properties

A step header is a bare name: line with nothing after the colon. Write it at the same indentation as the other steps: two spaces under FLOW:. The parser also reads a header at column 0, even with trailing spaces. The parser does not read these lines as a step header:
  • A header at any other indentation.
  • A header at either accepted column that is followed by a comment.
  • A two-space header that is followed by trailing spaces.
When step properties follow such a line, the parser reports warning W217 for the common shapes. The lines under it are read as part of the step above, where they can replace properties that step already has. Each single-value step property holds one value. These include REASONING, GOAL, THEN, ON_INPUT, ON_RESULT, CALL, and RESPOND. Writing one a second time at the step’s own property level reports warning W218. These properties do not trigger W218:
  • SET, DO, LOG, STEP_CONSTRAINTS, and BEHAVIOR accumulate.
  • ON_SUCCESS and ON_FAILURE blocks merge.
  • ON_FAIL fills a different slot for a CHECK and a CALL.
  • VOICE, RICH_CONTENT, and FORMATS merge their members.
Both warnings are advisory. The agent compiles and runs as parsed.

Per-step REASONING toggle

Every step in a FLOW: section must declare REASONING: true or REASONING: false. This controls whether the step uses LLM reasoning or deterministic execution.

Reasoning step properties

When REASONING: true, the following additional properties are available: Validation rules:
  • A step with REASONING: true must have either a step-level GOAL or an agent-level GOAL: defined.
  • A step with REASONING: false shouldn’t have a GOAL (it has no effect on deterministic steps).
  • A step with REASONING: false shouldn’t have AVAILABLE_TOOLS (use CALL to invoke tools deterministically).

Controlling direct context writes (CONTEXT_UPDATES)

CONTEXT_UPDATES controls direct model writes in a REASONING: true step: whether (and which) fields the model may write through the Runtime-owned __set_context__ tool. It doesn’t control what the model can read, which business tools it can call, or what trusted FLOW actions (SET, on_result mappings), authored GATHER extraction, and hooks can change. Those are separate, unaffected contracts.
In YAML, use context_updates: [qr_text, qr_done] directly on the reasoning step (lowercase, same list syntax). List-form rules:
  • Lists are supported only on REASONING: true FLOW steps (not the agent-level default).
  • Every list member must be a field already declared in MEMORY.session. Even if a GATHER or EXIT_WHEN reference would otherwise make an implicit field writable, it still needs its own MEMORY.session declaration to be listed. Declaring it there also makes it eligible in other, unscoped steps.
  • A list that excludes a field a GATHER or EXIT_WHEN needs written can still work, but only if that field gets its value from another source (prefilled state, or a trusted authored write like SET/on_result). Otherwise the step can never progress, because the model cannot write it directly.
  • A list narrows; it never grants new authority. READONLY and MODEL_WRITERS (and other Runtime-owned protections) still apply on top of it. Listing a field in CONTEXT_UPDATES does not bypass those.
  • An EXIT_WHEN reference does not implicitly add its field to the list. Include a model-written exit field explicitly, or supply it through a trusted authored action instead.
  • Names are case-sensitive and are normalized by trimming, deduplicating, and sorting; execution-tree paths and granted-memory keys cannot be list members.
  • CONTEXT_UPDATES: [] is different from omitting the field: omission keeps legacy full-write access; [] is a declared “no direct writes at all.” [] also restricts the separate completion-fields context-update path, which plain false does not. This is the one sharp edge between the two opt-out forms.
A reasoning step whose only job is to present already-selected state can declare CONTEXT_UPDATES: false so the model is never offered a write tool it has no reason to use:
Any list declaration, including CONTEXT_UPDATES: [], requires compiler and Runtime support for IR version 1.3. An older Runtime rejects an artifact built with one. Existing projects get no automatic change: a project must opt in by adding a list to a step and publishing only after every Runtime pod serving that project has been upgraded (the compiler does not block publishing out of order; that ordering is a deployment-operations responsibility). Rolling back to an older Runtime requires an artifact with no list declarations.

Per-step EXECUTION

A REASONING: true step can set its own model and generation settings with an EXECUTION: block indented under the step. Keys the step leaves out keep the agent EXECUTION value. The settings apply only to the calls that produce the step’s answer. Extraction, classification, and other steps keep the agent settings.

Step EXECUTION parameters

Values are never converted. A quoted number such as "0.2" is text and raises W565. A number outside its range raises W564. In both cases the step uses the agent value for that key.
Write one key per line, all at the same indent. Put fallback_model and conversation_history_window in the agent EXECUTION section instead. They raise W567 in a step.
  • The step model outranks the agent’s Model Config tab and its EXECUTION model. It must be a model the project can run. If not, the step uses the agent’s model and the trace records model_access:step_model_fallback. A deployment model override still wins.
  • A mistake in the block is a warning (W563 to W569 and W573, STEP_EXECUTION_*). The bad key is ignored and the other keys apply, so the agent still compiles.
  • The block has no effect on a step that is not a REASONING step, or on a speech-to-speech voice session.

Choosing the next step

A step runs whenever the flow reaches it. To take one path or another based on a value collected or computed earlier, route from a decision step with ON_RESULT. It works on a step without a CALL. The first matching branch wins:
A step-level WHEN: does not skip the step. It has no effect, and the compiler reports the warning FLOW_STEP_WHEN_IGNORED. Remove it and route with ON_RESULT, or use CHECK with ON_FAIL.

Attempt limiting

Steps can limit the number of times they execute (useful for retry loops):
The runtime counts an attempt in these cases:
  • A failed required-input collection.
  • An authored ON_INPUT retry that returns to the protected step, including a cycle through deterministic helper steps.
  • A reasoning customer turn that ends without satisfying EXIT_WHEN.
How the count works:
  • When the same customer message triggers a same-step ON_INPUT retry and then reasoning, the two paths use one attempt in total.
  • A customer message routed through ON_INPUT and deterministic helper steps counts once when a different reasoning step evaluates it. Automatic reasoning without a customer message does not count.
  • The counter is durable across session serialization and belongs to the active agent thread. A successful exit clears the current step’s failed-attempt counter.
  • MAX_TURNS is a separate limit on model and tool cycles inside one invocation.
  • For a non-terminal ON_EXHAUSTED recovery target, the runtime keeps the final count through the exhaustion transition. It clears the count when you re-enter the protected step to start a new cycle. A self-target starts a fresh cycle after its exhaustion event.
  • A terminal ON_EXHAUSTED outcome of COMPLETE or ESCALATE keeps the final count for diagnostics.
  • Restoring a parked step whose count is below its limit keeps the interrupted retry budget. Restoration alone does not start a new cycle.

Step actions

SAY / RESPOND

The RESPOND: action sends a message to the user. It supports template interpolation with {{variable}} syntax:
Multi-line responses use pipe block syntax:

MODEL_CONTEXT on a response

An optional MODEL_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:
This is useful when the delivered response carries a large presentation payload (a widget, a card, a big table) that would otherwise bloat every later model call for no benefit — 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

The PRESENT: 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:
Upper-case keys at that level (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.

Inline and bare forms

For fields that need no other properties, write the names on the GATHER: line, or as bare list items:
A bare list item, such as - email, is a required field. The inline forms GATHER: a: required, b: optional are also accepted. A list item that ends in default=value sets required: false and a default, and always gives warning W822.

Inheriting a top-level field

A step field inherits the properties of a top-level GATHER field with the same name. The step overrides only the properties it sets. A step field that sets nothing but required or optional, such as - notes: optional, does not change the inherited required value. Add another property, such as prompt, to override it.

Multi-field collection

Collect multiple fields in a single step using the FIELDS: sub-block:

Flow gather field properties

Each field in a flow GATHER: supports these properties:

Voice delivery for a field

On KoreVG pipeline voice, a field can carry its own VOICE block with BARGE_IN and STREAMING. The block governs that field’s prompt, reprompts and readback. Use it to protect a prompt the caller must hear in full while the rest of the step stays interruptible.
  • Precedence. A field’s policy is narrower than the step’s VOICE block, the agent’s CONVERSATION.listening.barge_in and the channel setting. An explicit allow on a field wins over a wider disallow, including a call-wide one.
  • Where it applies. Only fields of a FLOW step GATHER: read the block. A VOICE line on a field of a top-level GATHER: section gives warning W170.
  • Errors. An invalid BARGE_IN is VOICE_INVALID_BARGE_IN. A non-boolean STREAMING warns with VOICE_INVALID_STREAMING. LISTENING is VOICE_LISTENING_CALL_LEVEL_ONLY. Keypad settings belong in the field’s DTMF: block.
  • Runtime support. A field’s BARGE_IN is published with the IR marker voice_output_policy. A runtime without the marker refuses the agent at publish, deploy and activation.
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. Both validation: and validate: are accepted in a flow field. Write transient for a field in a top-level GATHER only: the runtime does not clear a transient field in a flow step.
A flow step asks only for required fields. Optional fields are captured when the customer volunteers them and are never asked. A step with only optional fields passes on entry. Put ON_INPUT branching in the step after the GATHER step. A step that has both gives warning FLOW_ON_INPUT_GATHER_ORDER_AMBIGUITY. A multi-line VALIDATE block is ignored with warning W174, and a VALIDATE object or list is used literally as a regular expression with warning W175.

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 warning GATHER_ASR_SHARED_STEP and uses the normal settings when asked together.
  • FLOW steps only. ASR: on a top-level (reasoning) GATHER field is preserved but has no effect, with compile warning GATHER_ASR_REASONING_UNSUPPORTED.
  • Deploy checks. A provider that 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:
Rules:
  • 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. A user.* value is personal data and needs restore_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.
Keep the list short and spoken-form (20 to 50 words is the range Deepgram recommends). Rank or cut the list in the tool, expand abbreviations, and use the form a caller says (“North Main Street”, not “N MAIN ST”). Never put session data in deepgramOptions.tag.
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:
  1. The field sets vocabulary.restore_pii: true.
  2. The project’s PII pattern for that kind of data has an explicit speech_provider rule 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.
A restored word is sent only to the recognizer, never written to traces or logs, and each restore is recorded in the PII audit log without the value. On Deepgram, a call that sends a restored word also opts the request out of Deepgram’s model-improvement program. 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:
The block can also be written over several lines (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. min above max is a compile error. A DTMF: block with no settings is a compile error.
  • min above 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.
  • VALIDATE checks the stored value. A type: phone field is stored in international format (for example, +919876543210), so ^[0-9]{10}$ never matches it. Use type: string to check digits exactly as typed, or write the pattern for the international format.
  • DTMF: doesn’t change validation or retries: use VALIDATE, RETRY_PROMPT, MAX_RETRIES on the same field, plus the agent’s ON_ERROR handler for validation_error with SUBTYPE: 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), use SECURE_INPUT instead.
  • Explicit keypad collections (SECURE_INPUT, the ivr_digit_input tool, 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 (for example, 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

Alongside FIELDS:, a flow GATHER: block accepts these block-level properties: The STRATEGY: value controls the collection approach:

CORRECTIONS

When CORRECTIONS: true, the user can naturally correct previously collected values without restarting the step:
When a CORRECTIONS: true GATHER accepts a changed value, the caller hears a short acknowledgement before the next prompt. It uses two message keys:
  • gather_correction_applied: default “Got it, I’ve updated the .”
  • gather_correction_dependents_cleared: default “I may need to ask you again about the .” The runtime sends it when the correction cleared dependent fields that the step asks for again.
Each key resolves like the other MESSAGES keys: project locale assets, then agent MESSAGES, then the English platform default. When the interaction language is not English and no localized text exists, the runtime omits that sentence.
  • {{fields}} and {{cleared_fields}} are the only placeholders. They hold a list of field labels joined for the interaction language. Any other placeholder renders as empty text, so the message never contains a collected value.
  • A field label is the gather_field_label_<field> message when you author one, for example gather_field_label_date_of_birth: "birthday". Otherwise it is the field name turned into lower-case words (incident_location becomes “incident location”), which is not translated.
  • A label in agent MESSAGES applies in every interaction language. Put per-language labels in the project locale assets.
  • Fields with PROMPT_MODE: extract_only are never named. The runtime sends each sentence only when its list of labels is not empty.
  • The acknowledgement is part of the turn’s response and transcript, ahead of the next prompt or the step’s closing reply.

COMPLETE_WHEN

The COMPLETE_WHEN: condition specifies when the gather step is considered complete:

CALL…WITH…AS

The CALL: action invokes a tool. Use WITH: to pass parameters and AS: to bind the result to a variable:
A key missing from a record reads as null inside a WITH expression. An argument with no value is left out of the tool input. See Missing values in CALL arguments.

WITH parameters

The WITH: block maps tool parameter names to values or variable references:
Values can be:
  • Variable references: amount (resolves the session variable amount)
  • 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:
Nesting is preserved by indentation. Scalar leaves keep their raw form and are coerced at runtime ("30" → 30, "[15, 16]" → [15, 16]).

Localized WITH arguments

A WITH 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:
If a referenced key isn’t defined for the caller’s locale (and no less-specific locale in the chain provides it), the tool is not called — the 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

The SET: action assigns values to session variables:
Each assignment uses 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()
An unquoted JSON array or object literal is stored as a structured value. A JSON string’s string-typed leaves may contain {{variable}} templates — the collection is parsed first, then its string values are rendered, so substituted data is never reinterpreted as JSON:
This stores an array of objects correctly even when employer_name contains quotes or backslashes. Prefer a direct variable reference when constructing a computed collection instead of interpolating one into JSON-shaped text:
Surround the whole value with quotes when JSON-shaped text is intentional — 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).
Behavior change. A computed SET value that cannot be evaluated no longer stores its own formula text. The variable gets no value, and the trace records a warning event with code set_expression_not_evaluated. Set EXECUTION: set_expression_failure: keep_text to keep the old behavior. See When a SET value cannot be evaluated.

CHECK

The CHECK: action evaluates a condition. If the condition is false, execution transitions to the ON_FAIL: step:
The runtime evaluates CHECK when the step is entered. It runs after the step’s CLEAR, SET, LOG, and DO, and before its GATHER and CALL. CHECK tests values that already exist when the step starts.
  • A step SET applies before CHECK, so it also applies when CHECK fails. An attempt counter that CHECK reads relies on this. To assign a value only on success, put the SET inside an ON_RESULT branch.
  • CHECK cannot read the step’s own GATHER answer or CALL result. To decide on them in the same step, use ON_RESULT, which runs after GATHER and CALL.

CLEAR

The CLEAR: action removes variables from the session context:

TRANSFORM

The TRANSFORM: action filters, maps, sorts, and limits an array from the session context:

LOG

The LOG: action emits a trace-only diagnostic (not shown to the user). Use the inline or block form:

AWAIT_ATTACHMENT

The AWAIT_ATTACHMENT: action pauses the step until the user uploads a file.

SECURE_INPUT

The SECURE_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.
SECURE_INPUT is currently authored only in the legacy .agent.abl (line-based) format. The .agent.yaml parser does not yet accept this action. It also requires a live voice call and the SECURE_INPUT_ENABLED runtime setting (on by default; only the value false turns it off): without a live voice session the step takes ON_FAIL; with the setting set to false, the step is skipped entirely and the trace records secure_input_disabled. Both the delivery CALL and any validation CALL must be HTTP project tools (configured in Project Tools, not the agent’s own TOOLS: section) that send a JSON body.
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.
Every outcome route (ON_SUCCESS, ON_TIMEOUT, ON_CANCEL, ON_FAIL) takes a step name only — a RESPOND or any other block indented under a route isn’t part of SECURE_INPUT; the parser reports the ignored line (W208) and an empty route is a compile error (SECURE_INPUT_EMPTY_OUTCOME_ROUTE). Put the response in the step the route names, as in the confirm step above.
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, VALIDATE, PROGRESS_PROMPT, NOT_SECRET, and SAVE_AS. 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.
Every collected value lives only in a local secure buffer for the duration of the gate and is zeroized on every exit path (success, timeout, cancel, or failure) — it is never written to session state, so it cannot be referenced by {{ }} outside the delivery/validation calls, and it never appears in a trace, span, or log.

Hold line while a CALL runs

PROGRESS_PROMPT gives the caller something to hear while a secure CALL waits for its answer. On the block, the runtime speaks it during the delivery CALL. On a field, it speaks it during that field’s VALIDATE CALLs. Built-in checks such as luhn finish at once and never speak it. The line plays with barge-in off, nothing is collected while it plays, and recording stays paused.
PROGRESS_PROMPT: "{{locale.PAYMENT_PROCESSING}}" on one line is the text alone. The line is chosen for the caller’s language the same way as a field PROMPT.
  • A repeat never starts before the previous line has finished. The runtime attempts at most 12 lines during one CALL.
  • A field with several VALIDATE CALLs speaks its line for each one that runs long enough.
  • With the block default AFTER: 0s, the line always plays and the reply waits for it to finish (up to about 4 seconds). For a fast payment API, use AFTER: 2s.
  • Hold lines resolve when the step starts, so a value saved during the step with SAVE_AS is not available to them.
  • When the CALL returns, the step lets the last line finish before it speaks the next prompt or reply.
  • A hold line that cannot be resolved does not fail the step. The runtime skips it and the trace records secure_input_progress_unresolved. On the block, the project’s tool-call filler line plays instead.
  • TEXT and REPEAT_TEXT can use session values and locale entries, but never {{secure.*}}.

Readable values (NOT_SECRET)

A value that is not card data, such as the billing ZIP code, can be collected in the same step and saved for the agent. Mark the field NOT_SECRET: postal_code and name the session variable with SAVE_AS:
The caller types the value with recording paused, like every other field. When it passes its checks, the runtime writes it to billing_zip as text and keeps a leading zero. From then on it is an ordinary session value. Later steps, reports, and tools read {{billing_zip}}. It is not part of the step’s secure values, so a response field that echoes it is kept.
  • The runtime clears each SAVE_AS variable when the step starts, so a value from an earlier run is never used. If it cannot clear the variable, the step takes ON_FAIL before it asks anything.
  • The saved value must be digits only. Anything else, or a session write that is not applied, takes ON_FAIL before the delivery.
  • postal_code is the only readable kind. The compiler rejects NOT_SECRET with another kind, without SAVE_AS, with READ_ONCE, or with a luhn or date_mmYY check. It also rejects it when COLLECT allows fewer than 5 or more than 10 digits, or when the field is named like card data or a PIN (card_number, pan, cvv, cvc, cvv2, csc, security_code, expiry, exp, pin, passcode, and similar). A card number, expiry, security code, or PIN always stays secret.
  • The runtime applies the same rules again and takes ON_FAIL before it asks anything if they are broken.
  • Only the field’s own VALIDATE CALL can read {{secure.<field>}} for a NOT_SECRET field. Every later CALL reads the SAVE_AS variable.
  • The step keeps at least one secret field, and the delivery sends at least one of them.

MESSAGE_KEY and BEHAVIOR

Two additional step-level properties:
  • MESSAGE_KEY: — a locale catalog key used to localize the step’s RESPOND: message.
  • BEHAVIOR: — free-form guidance text appended to the step’s reasoning goal (a convenience alias that folds into GOAL).

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. Write an explicit THEN target on every authored branch. A branch without one does not survive the compiler round trip. For older compiled artifacts that have no branch target, the runtime keeps compatibility: a matched branch applies its actions and waits at the current step. It does not fall through to the enclosing step’s response or continuation. Do not author branches without THEN. An ON_RESULT response is an authored assistant message.
  • If the THEN target asks another question, the branch response comes before that question in the turn’s output. Each response keeps its own presentation and history entry, so advancing to a GATHER does not replace the earlier answer.
  • A matched branch keeps its own continuation. It does not run the enclosing step’s default response or continuation.
  • Branch response text uses the configured output guardrails and PII protection. Structured display text is protected before delivery and history, including values found by asynchronous PII recognizers. Action values remain reversible vault tokens. Without a vault they keep their existing round-trip value.
  • For payload blocks nested directly under an inline branch RESPOND, write VOICE first, then FORMATS (or RICH_CONTENT), then ACTIONS. The voice configuration belongs to that response, including when it comes before another step’s prompt.

ON_SUCCESS / ON_FAIL

An alternative to ON_RESULT: for simpler success/failure branching:
Both ON_SUCCESS: and ON_FAIL: support conditional branches:

ON_INPUT

ON_INPUT: provides branching based on user input, typically used after a GATHER: action:
A step that has only ON_INPUT branches — no RESPOND/prompt and no default (ELSE) branch — traps any unmatched input on the same step forever. The compiler warns (FLOW_PROMPTLESS_ON_INPUT_UNMATCHED_TRAP): add a default ELSE route or an authored prompt unless the wait is intentional.
ON_INPUT and ON_RESULT conditions also bind turn_input. It holds the caller message of the current turn, the same value RESPOND renders as {{input}}. input holds the message the step owns. When a transition in the same turn enters the step, input is empty and turn_input keeps the caller message.

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 (under global_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 to RESPOND: messages and handled with ON_ACTION: callbacks.

Action elements

Interactive actions are defined in an ACTIONS: block within a step:

Element types

Buttons

Select (dropdown)

Input fields

Action element properties

Form submission

When a step contains input type elements, you can configure a form submission button:
The ACTIONS: block also accepts an optional RENDER_ID: to give the whole action set a stable identifier for rendering/telemetry. To place inputs, buttons, and text side by side, use ROW, SUBMIT, and TEXT items. See Form layout.

ON_ACTION callbacks

The ON_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 alongside RESPOND: 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:
The same keys are allowed in a nested VOICE: block under every reply a step can produce: ON_SUCCESS, ON_FAILURE, ON_RESULT and ON_INPUT branches, SUB_INTENTS, DIGRESSIONS and ON_ACTION DO actions. They’re also allowed on a GATHER field (Voice delivery for a field) and in the step’s ON_NO_INPUT. Precedence. From narrowest to widest: the output’s own VOICE, its template, its GATHER field, the tool reply policy, the step, the agent’s CONVERSATION.listening.barge_in, then the channel setting. An absent value inherits the next layer. An explicit allow wins over a wider disallow, including a call-wide one. It can also be set on a named template’s VOICE: block. Placing BARGE_IN in a behavior-profile VOICE block, a channel-override VOICE block, a free-standing ON_START or branch VOICE block, or YAML execution.voice is rejected (VOICE_BARGE_IN_ILLEGAL_PLACEMENT). In ABL EXECUTION.voice and MESSAGES, BARGE_IN and STREAMING are ignored with warning VOICE_DELIVERY_KEY_IGNORED_PLACEMENT. STREAMING in the older placements compiles with VOICE_STREAMING_LEGACY_PLACEMENT. In a constraint ON_FAIL, any voice line warns with CONSTRAINT_ON_FAIL_VOICE_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.
Omitting streaming inherits the next layer. At the end of the chain the default is the producing agent’s model streaming setting, which streams unless the agent sets it to false.
  • Derived turn-taking. When the call’s listening mode is derived as turn_taking (barge-in off and no explicit mode), output is buffered, and an authored STREAMING: true does not change that.
  • Connection Streaming switch. When the channel connection’s Streaming switch is Off, ordinary output is buffered in every mode, including output that authors STREAMING: true. Bring-your-own streaming-only speech vendors ignore the switch.
  • Vendor limits. The effective value is the requested value limited by the speech vendor and delivery path. Regular speech providers play all turn_taking output buffered.
  • Secure input. Secure-input prompts and progress speech can stream even when the connection Streaming switch is Off.

Step-level no-input (caller silence)

A FLOW step can carry its own caller-silence policy in its own VOICE: 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_ms and inherit everything else. max_retries, relaxed_interval_ms, on_exhausted, and exhausted_message all follow the same rule, and enabled: true on 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, and exhausted_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.
  • Delivery. barge_in (allow or disallow) and streaming (true or false) in ON_NO_INPUT apply to the step’s reprompts and its exhausted message. A reprompt resolves the ON_NO_INPUT value, then the step’s own VOICE: BARGE_IN or STREAMING, then the turn default. A step that authors either key is published with the IR marker voice_no_input_policy. A runtime without the marker refuses the agent.
ON_NO_INPUT is only allowed directly in a FLOW step’s own VOICE: block. Placed under a GATHER field, RESPOND, a branch, a handler, or a response format, it’s a compile error (VOICE_NO_INPUT_ILLEGAL_PLACEMENT). The YAML format uses a step-level on_no_input: key.

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-level ON_ERROR: handlers:
The error type (the handler’s key) is matched against a fixed, canonical set — 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 a handler with SUBTYPE/SUBTYPES. Known values: rate_limit, auth_failure, network_error, tool_timeout for tool_error, and timeout, content_filter, rate_limited, context_exceeded, api_error for llm_error.

List form and map form

Write a step’s handlers as - TYPE: list items, or keyed by type like agent-level handlers. Both forms are equivalent:
A type can repeat. Handlers match subtype first, so the timeout entry wins for a timeout and the second llm_error entry catches the listed subtypes.
  • A step handler takes SUBTYPE or SUBTYPES, a one-line RESPOND, any number of SET: name = expression lines (applied before RESPOND is rendered), RETRY, RETRY_DELAY, RETRY_BACKOFF, THEN, BACKTRACK_TO, and HANDOFF_TARGET.
  • VOICE, FORMATS, multi-line responses, and TEMPLATE(name) references work on agent-level handlers only.
  • Which handler runs. The step’s handlers are checked first, then the agent’s, then the agent’s default handler. Within each, a subtype match wins over a type match, which wins over DEFAULT. A matching step handler replaces the agent-level handler for that step, including its RESPOND. When no handler matches a model error, a declared unknown_error handler is used.
  • Where step handlers apply. They cover a REASONING: true step’s own model calls, its scripted CALL:, and its other step-owned errors. An error from a tool the model calls, and a validation_error after a field’s MAX_RETRIES, use the agent-level handlers only.

When a reasoning step’s model call fails

A model error in a REASONING: true step (llm_error: a timeout, a content filter, a rate limit, and other model failures) first runs the handler’s RETRY, if any. If the call still fails, THEN decides what happens on that turn:
  • Without a RESPOND. BACKTRACK and COMPLETE send no error line: the target step, or the completion, speaks. CONTINUE, RETRY_STEP, and a BACKTRACK naming the failing step send the platform fallback line below. HANDOFF lets the target answer.
  • RETRY versus RETRY_STEP. RETRY: N repeats the failed model call up to N more times in the same turn. Only a model timeout, a rate limit, and an unavailable provider are retried. A retry is not attempted once the failed attempt has streamed part of its reply. RETRY_STEP waits for the customer’s next message.
  • Model time limit. A call times out when the step’s EXECUTION: llm_timeout (else the agent’s) runs out, capped by the server limit and the turn’s remaining time. A caller cancellation is not a timeout and runs no handler.

Platform fallback lines

When a handler has no RESPOND and needs one, the customer sees the agent’s message for the error’s subtype: Replace a line for one agent in MESSAGES:, or per language in locales/<locale>/<agent>.json (one agent) or locales/<locale>/_shared.json (all agents). A locale file wins over MESSAGES, which wins over the default.

Step ON_ERROR handler properties

Complete example


Related articles:
  • Language overview — syntax rules and auto-detection
  • Tools — tool definitions used by CALL actions
  • GATHER — top-level gather field definitions
  • Agent declaration — GOAL used by REASONING: true steps, max_flow_iterations and model settings