# Agent Blueprint Language (ABL)
Source: https://koreai.mintlify.app/agent-platform/abl
Agent Blueprint Language (ABL™) is the typed, schema-driven language used to define agents on the platform. Every agent — its identity, tools, memory, routing, constraints, and guardrails — is expressed in a single ABL definition. The compiler validates it. The runtime executes it across both cognitive engines.
ABL definitions are YAML artifacts. The same blueprint compiles to the same abstract syntax tree (AST) and intermediate representation (IR), whether authored directly in ABL syntax or generated by Arch™ AI.
## What ABL defines
A single ABL definition covers the full specification of an agent. Each construct maps to a runtime capability enforced by the compiler.
| Construct | What It Specifies |
| - | - |
| **Identity** | Agent name, type, persona, and goals. |
| **Execution** | LLM model, temperature, iteration limits, and timeout. |
| **Tools** | Typed function signatures the agent can call, with input/output contracts. |
| **Memory** | Short-term session state and long-term persistent state. |
| **Handoffs** | Typed routing to specialist agents, including context grants and return expectations. |
| **Delegates** | Inline sub-agent calls that answer a sub-question without transferring thread ownership. |
| **Constraints** | `REQUIRE` / `ON_FAIL` business rules enforced at runtime. |
| **Guardrails** | Input and output policy checks — PII redaction, topic scope, hallucination guards — compiled into the agent, not added externally. |
| **Flow** | Optional step-by-step orchestration for interactions where the execution path is completely deterministic. |
## Compilation pipeline
ABL definitions move through three stages before they execute. The ABL source is the diffable artifact stored in version control. The compiler produces a portable IR that is versioned and pinned to a deployment, and can be downloaded. The runtime executes the IR — not the raw ABL source.
```mermaid actions={false} theme={null}
%%{init: {'theme': 'base', 'themeVariables': {'background': 'transparent', 'primaryColor': '#EAF8E4', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#5CC83A', 'lineColor': '#5CC83A', 'secondaryColor': 'transparent', 'tertiaryColor': 'transparent', 'clusterBkg': 'transparent', 'clusterBorder': '#5CC83A', 'titleColor': '#1a1a1a', 'clusterLabelBackground': 'transparent', 'edgeLabelBackground': '#EAF8E4'}}}%%
flowchart LR
A["ABL Definition\n(.agent.abl / YAML)\ndiffable"] --> B["ABL Compiler\nvalidates schema\nchecks references\nenforces contracts"]
B --> C["IR\nportable artifact\nversioned · pinned to deployment · downloadable"]
C --> D["ABL Runtime\nexecutes both\ncognitive engines"]
```
| Stage | What Happens |
| - | - |
| **ABL Compiler** | Validates the full definition. Catches missing tool references, broken handoffs, contract mismatches, and guardrail gaps before deployment. |
| **IR** | The compiled artifact. Environment-agnostic — the same IR runs in dev, staging, pilot, and production without modification. |
| **ABL Runtime** | Executes the IR. Runs the Agentic Brain for reasoning steps and the Deterministic Brain for constraint-enforced paths. Both share the same runtime. |
Compile-time validation runs on every commit. Broken definitions never reach a downstream environment.
## ABL compared to framework code
On framework-based stacks, building a production agent means stitching together prompts, code, and connectors across multiple languages and SDKs. The orchestration logic seems like glued code that's difficult to review, difficult to test, and brittle at scale.
ABL replaces glued code with a single declarative definition. Constructs like `HANDOFF`, `DELEGATE`, `FAN_OUT`, and `GUARDRAILS` are first-class primitives that the compiler enforces and the runtime executes, rather than patterns re-implemented per project.
| Capability | Framework Code | ABL |
| - | - | - |
| **Compile-time validation** | Not available — errors surface at runtime. | Built in — Detect missing tools, broken references, and contract violations before deployment. |
| **Version control and diff** | Mixed across code, prompt files, and configuration — diffs are noisy. | Clean YAML diffs — agent changes review like application code. |
| **Cross-runtime portability** | Locked into the framework's runtime. | IR-based — the same definition runs anywhere the runtime is deployed. |
| **AI authoring** | Fragile — AI-generated framework code breaks at scale. | Reliable — ABL has a strict schema that AI can generate, validate, and modify consistently. |
| **Governance** | Per-project — each team reinvents guardrails. | Uniform — one policy layer across every agent, regardless of who authored it. |
## Author ABL
You can author ABL in three ways and all produce the same compiled output.
| Method | How It Works |
| - | - |
| **Arch AI** | The default experience. Arch AI interviews stakeholders, proposes agent topology, generates ABL, and validates the output against the compiler before presenting it for human review. No manual ABL writing required. |
| **Studio IDE** | The Monaco-based code editor in Agent Studio. Provides syntax highlighting, inline diagnostics, and an outline view for navigating large definitions. |
| **External tools** | Claude Code, Cursor, and Codex connect to the platform through MCP. Agents can be authored from a CLI or IDE using the same MCP interface as the Studio. |
## ABL example
The example below shows a funds-transfer agent. It illustrates the key constructs — identity, execution parameters, typed tools, memory scope, handoffs, constraints, guardrails, and flow steps. The `REASONING: true` flag on a flow step activates the Agentic Brain for that step; `REASONING: false` runs the Deterministic Brain.
```yaml expandable=true theme={null}
AGENT: Funds_Transfer_Agent
VERSION: "3.1.0"
DESCRIPTION: "Orchestrates domestic and international funds transfers with fraud
screening, compliance checks, FX conversion, and full audit trail."
GOAL: |
Guide the customer through a secure funds transfer end-to-end.
Verify identity, check limits, screen for fraud, apply FX if needed,
get explicit confirmation, execute, and notify all parties.
Escalate to a human advisor for high-value or flagged transfers.
EXECUTION:
model: claude-opus-4-7
temperature: 0.1 // low temperature — financial precision required
max_turns: 12
timeout_seconds: 180
TOOLS:
verify_customer(customer_id: string, mfa_token?: string)
→ { verified: bool, tier: string, daily_limit: number }
screen_fraud(customer_id: string, transfer: object)
→ { risk_score: number, flags: array, decision: string }
confirm: never // always runs silently
execute_transfer(customer_id: string, from_account: string,
to_account: string, amount: number, currency: string)
→ { transfer_id: string, status: string, eta: string }
confirm: always // always prompts customer before executing
MEMORY:
session:
- customer_id
- from_account
- to_account
- amount
- currency
- fraud_result
- transfer_result
HANDOFF:
- TO: Senior_Advisor_Agent
WHEN: amount > 50000 OR fraud_result.decision == "REVIEW"
CONTEXT: { pass: [customer_id, transfer_details, fraud_result] }
RETURN: true
CONSTRAINTS:
always:
- REQUIRE customer.verified == true
ON_FAIL: HANDOFF Authentication_Agent
- REQUIRE fraud_result.decision != "BLOCK"
ON_FAIL: RESPOND "This transfer has been flagged and cannot proceed."
THEN: COMPLETE
GUARDRAILS:
input:
topic_scope:
allowed: [funds_transfer, account_balance, fx_query, dispute]
on_out_of_scope: RESPOND "I can only assist with funds transfers and related queries."
output:
mask_account_numbers:
pattern: account_number_pattern
action: show_last_4_only
FLOW:
entry_point: verify_identity
steps: [verify_identity, collect_transfer_details, screen, confirm, execute]
verify_identity:
REASONING: false
CALL: verify_customer(customer_id)
ON_SUCCESS: THEN collect_transfer_details
ON_FAIL: HANDOFF Authentication_Agent
collect_transfer_details:
REASONING: true
GOAL: "Collect from_account, to_account, amount, currency, optional reference."
EXIT_WHEN: from_account IS SET AND to_account IS SET AND amount IS SET
MAX_TURNS: 4
THEN: screen
```
## Key properties
ABL is designed so that agents are software artifacts, not configuration. Four properties follow from that design:
* **AI-writable** — The strict schema allows Arch AI to generate, validate, and modify ABL reliably. Authoring through Arch AI is the default experience.
* **Versioned** — Every change to an agent definition produces a reviewable diff in version control, like a change to application code.
* **Diffable** — Agent behavior changes are visible in pull requests. The prompt is not the artifact; the ABL is.
* **Auditable** — The compiled IR is the immutable record of what was deployed. Compiler validation runs on every commit, so the IR in production always matches the reviewed definition.
***
**Related articles:**
* [ABL Language Overview](/agent-platform/abl-reference/language-overview)
# How to choose between a reasoning agent and a FLOW-based agent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/choose-reasoning-agent-or-flow
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this guide to decide whether an agent should be goal-driven and flexible, step-driven and predictable, or a mix of both.
## Concept
An Agent Business Language (ABL) agent can run without a `FLOW:` block or with one.
A reasoning agent has an `AGENT`, a `GOAL`, optional persona or limitations, optional tools, optional gathered fields, and completion rules. Without a `FLOW:` block, the reasoning layer chooses the next best action within the defined boundaries.
A flow-based agent adds a `FLOW:` block and named steps. The flow controls the conversation path. Each step declares whether it is deterministic with `REASONING: false` or allows a bounded reasoning area with `REASONING: true`.
Use a reasoning agent when the work can be expressed as a business goal and the conversation can adapt. Use a flow when the order of operations, checkpoints, or troubleshooting path must be explicit. Use a workflow tool, not a flow, for long waits, polling, scheduled work, or multi-hour orchestration.
## Decision guide
| Scenario | Use | Why |
| - | - | - |
| Flexible Q\&A, policy explanation, broad triage | Reasoning agent | The agent can choose how to answer from the goal, tools, limitations, and context. |
| Required intake or compliance sequence | `FLOW` with `REASONING: false` steps | Each field, prompt, response, and next step is explicit. |
| Scripted process with one conversational decision | `FLOW` with a bounded `REASONING: true` step | The flow owns the process while the reasoning step handles interpretation. |
| Durable approval, polling, callback, scheduled follow-up | Workflow tool | Long-running state belongs outside the stateless agent flow. |
## Minimal working example: reasoning agent
This example is a single agent file. It leaves the conversation flexible but gives the agent a required topic and a safe completion condition.
```yaml expandable=true theme={null}
AGENT: Policy_QA_Agent
GOAL: "Explain employee policies and identify when HR must handle the request"
PERSONA: |
Clear, concise HR policy assistant.
Explains policy rules and next steps without approving exceptions.
LIMITATIONS:
- "Cannot approve policy exceptions"
- "Cannot change employee records"
GATHER:
policy_topic:
prompt: "Which policy do you need help with?"
type: string
required: true
COMPLETE:
- WHEN: policy_topic IS SET
RESPOND: "I can explain the {{policy_topic}} policy and tell you when HR must review the request."
```
Use this pattern when there is no required path beyond collecting the information needed to answer safely. Avoid using `WHEN: true` as a normal completion rule because it can make the example appear complete even when the customer outcome is not reached.
## Working example: deterministic FLOW for required intake
This example is a single agent file. It collects warranty information in a fixed order, then completes only after the required fields are present.
```yaml expandable=true theme={null}
AGENT: Warranty_Intake_Agent
GOAL: "Collect warranty intake details in a fixed order"
FLOW:
entry_point: collect_serial
steps:
- collect_serial
- collect_issue
- complete_request
collect_serial:
REASONING: false
GATHER:
- serial_number:
prompt: "What is the device serial number?"
type: string
required: true
THEN: collect_issue
collect_issue:
REASONING: false
GATHER:
- issue_description:
prompt: "What problem are you seeing?"
type: string
required: true
THEN: complete_request
complete_request:
REASONING: false
RESPOND: "Thanks. I have the serial number and issue description."
THEN: complete
COMPLETE:
- WHEN: serial_number IS SET AND issue_description IS SET
RESPOND: "Warranty intake is ready for {{serial_number}}: {{issue_description}}."
```
Use this pattern when the customer must not skip required fields or when operations teams need to inspect the exact conversation path.
## Working example: bounded reasoning inside a FLOW
This example is a single agent file. The reasoning step is limited to understanding the support path; the rest of the process remains explicit.
```yaml expandable=true theme={null}
AGENT: Support_Intake_Agent
GOAL: "Understand a support request, collect account context, and prepare the next action"
FLOW:
entry_point: understand_request
steps:
- understand_request
- collect_account
- finish_intake
understand_request:
REASONING: true
GOAL: "Clarify whether the customer needs billing, technical support, or human escalation"
EXIT_WHEN: "the support path and issue summary are clear enough to continue"
MAX_TURNS: 3
THEN: collect_account
collect_account:
REASONING: false
GATHER:
- account_id:
prompt: "What account should I use for this request?"
type: string
required: true
THEN: finish_intake
finish_intake:
REASONING: false
RESPOND: "I have the account context and the support path. I can continue with the next step."
THEN: complete
COMPLETE:
- WHEN: account_id IS SET
RESPOND: "Support intake is ready for account {{account_id}}."
```
Use this pattern when only one part of the process needs interpretation. Keep the reasoning zone narrow, give it an exit condition, and add a max-turn limit.
## Verification
1. Validate the agent file.
2. Run a sample utterance for the reasoning-agent example: "Can you explain the parental leave policy?"
3. Run the warranty flow with a serial number and issue description. Confirm the prompts happen in order and completion waits until both fields are present.
4. Run the bounded-reasoning flow with a broad request such as "I need help with a billing issue and my account." Confirm the reasoning step clarifies the path before collecting `account_id`.
5. Inspect trace/debug output for the active step name, gathered fields, and final completion response.
## Production readiness checklist
* Use a reasoning agent only when `GOAL`, `LIMITATIONS`, tools, and completion rules are enough to bound behavior.
* Use flow steps for required collection, compliance prompts, or processes support teams must inspect.
* Keep `REASONING: true` steps bounded with a clear `GOAL`, exit condition, and max-turn limit.
* Ensure every required gathered field feeds into completion, memory, a tool call, handoff, delegate, or another known consumer.
* Move long waits, polling, scheduled callbacks, and multi-hour orchestration into a workflow tool.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using `FLOW` for every conversation | Developers want control even when a goal is enough. | Start with a reasoning agent when the outcome is flexible and low-risk. |
| Using `WHEN: true` as the main completion rule | It makes examples succeed before the customer outcome is complete. | Complete on meaningful fields, tool results, or final state. |
| Adding a reasoning step that never affects the outcome | The flow has a reasoning step but no clear purpose. | Give the step a narrow goal and explain what it prepares for the next step. |
| Using agent flow for durable work | Flow is visible and convenient, but not meant for long waits. | Use workflow-as-tool for durable orchestration. |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| A required gathered field triggers warnings | The field is never consumed. | Reference it in completion, memory, a tool input, handoff, delegate, or next-step logic. |
| A reasoning step keeps asking questions | The exit condition is too broad or the max-turn limit is missing. | Tighten `GOAL`, `EXIT_WHEN`, and `MAX_TURNS`. |
| The flow is hard to maintain | Too many business paths are in one agent. | Split into specialists or move durable work to a workflow. |
# How to decide whether to build one agent or multiple specialist agents
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/choose-single-agent-or-specialists
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this guide when one customer-facing experience could be implemented either as one broad agent or as a supervisor with multiple specialist agents.
## Concept
A single agent owns the conversation and the outcome. It can gather information, call tools, answer, and complete the request without transferring ownership.
A specialist design separates the front door from the business owners. A supervisor receives the user request, chooses the right owner, and uses `HANDOFF` when another agent should take over. A specialist owns one capability, such as account access, order status, billing, claims, device care, or employee benefits.
The design question is ownership. If one team, policy set, permission model, and success criteria can own the outcome, keep one agent. If different teams, tools, policies, or escalation paths own different parts of the experience, use specialists.
## Decision guide
| Question | Prefer one agent when | Prefer specialists when |
| - | - | - |
| Scope | Requests are tightly related. | The entry point receives unrelated business requests. |
| Tools and data | The same tools and permissions apply. | Different topics need different tools or data access. |
| Risk | One policy set is enough. | Each topic has different compliance, escalation, or approval rules. |
| Ownership | One team updates and tests the whole experience. | Different teams own different business outcomes. |
| Routing | No transfer is needed. | The user needs to be routed to the right owner. |
Do not split agents just because a domain is large. Splitting adds routing, context-passing, target-agent readiness, testing, monitoring, and fallback design.
## Minimal working example: one agent owns the outcome
This example is one agent file. It is enough when order-status work has one owner and one required field.
```yaml expandable=true theme={null}
AGENT: Order_Status_Agent
GOAL: "Answer order status and delivery questions"
LIMITATIONS:
- "Cannot issue refunds"
- "Cannot change fulfilled orders"
GATHER:
order_id:
prompt: "What is your order ID?"
type: string
required: true
COMPLETE:
- WHEN: order_id IS SET
RESPOND: "I can help check the status of order {{order_id}}."
```
Use this shape when the same agent can safely handle the full request. If refund, billing, or human escalation becomes part of the same entry point, add explicit boundaries or split ownership.
## Working example: supervisor with specialist agents
This example is a project-level set. The supervisor and all referenced specialists must exist in the same project.
`agents/support-supervisor.agent.abl`
```yaml expandable=true theme={null}
SUPERVISOR: Support_Supervisor
GOAL: "Route support requests to the right specialist"
AGENTS:
account: Account_Support_Agent
orders: Order_Status_Agent
human: Live_Agent
INTENTS:
account: "Account access or password help"
order_status: "Order tracking or delivery help"
human_help: "User asks for a person or the request is unclear"
GATHER:
customer_id:
prompt: "What customer ID should I use for this request?"
type: string
required: true
HANDOFF:
- TO: Account_Support_Agent
WHEN: intent.category == "account"
PASS: [customer_id]
SUMMARY: "Customer needs account or password support"
RETURN: true
- TO: Order_Status_Agent
WHEN: intent.category == "order_status"
PASS: [customer_id]
SUMMARY: "Customer needs order status or delivery support"
RETURN: false
- TO: Live_Agent
WHEN: intent.category == "human_help"
PASS: [customer_id]
SUMMARY: "Customer asks for a person or the request is unclear"
RETURN: false
```
`agents/account-support-agent.agent.abl`
```yaml expandable=true theme={null}
AGENT: Account_Support_Agent
GOAL: "Resolve account access and password support requests"
GATHER:
customer_id:
prompt: "What customer ID should I use?"
type: string
required: true
COMPLETE:
- WHEN: customer_id IS SET
RESPOND: "I can continue account support for {{customer_id}}."
```
`agents/order-status-agent.agent.abl`
```yaml expandable=true theme={null}
AGENT: Order_Status_Agent
GOAL: "Resolve order status and delivery support requests"
GATHER:
customer_id:
prompt: "What customer ID should I use?"
type: string
required: true
COMPLETE:
- WHEN: customer_id IS SET
RESPOND: "I can continue order support for {{customer_id}}."
```
`agents/live-agent.agent.abl`
```yaml expandable=true theme={null}
AGENT: Live_Agent
GOAL: "Transfer the customer to a human support queue"
COMPLETE:
- WHEN: true
RESPOND: "I will connect you with a human support specialist."
```
`AGENTS:` is a readable roster. `HANDOFF` is the executable routing surface. `customer_id` is gathered before handoff and passed to the specialists with `PASS`.
Since none of the `HANDOFF` entries above declare `HISTORY`, each specialist now receives the full conversation history by default (the current platform default when `HISTORY` is omitted). Also, because each `WHEN` here is a single-field comparison (`intent.category == "account"`), an unset field simply makes the comparison evaluate `false` — no error, and the trace shows your literal source text.
## When to use handoff
Use `HANDOFF` when the target agent should own the next part of the conversation.
Use `RETURN: true` when the specialist should do bounded work and return control to the supervisor. Use `RETURN: false` when the target agent or human channel should keep ownership.
## When to use delegate
Use `DELEGATE` when the parent agent keeps the conversation but asks another agent for a result.
This example is a two-agent project set. The child produces `total_fee`, and the parent maps that result into `quoted_fee`.
`agents/booking-manager.agent.abl`
```yaml expandable=true theme={null}
AGENT: Booking_Manager
GOAL: "Manage booking changes while keeping ownership of the customer conversation"
GATHER:
booking_id:
prompt: "What booking should I use?"
type: string
required: true
DELEGATE:
- TO: Fee_Calculator
WHEN: "the user asks for a booking change fee"
PURPOSE: "Calculate the fee for the requested booking change"
PASS: { booking_id: booking_id }
RETURNS:
total_fee: quoted_fee
USE_RESULT: "Explain the fee before asking the user to confirm"
TIMEOUT: "10s"
ON_FAILURE: RESPOND "I could not calculate the fee right now."
COMPLETE:
- WHEN: booking_id IS SET
RESPOND: "I can manage booking changes for {{booking_id}}."
```
`agents/fee-calculator.agent.abl`
```yaml expandable=true theme={null}
AGENT: Fee_Calculator
GOAL: "Calculate booking change fees for the booking manager"
FLOW:
entry_point: calculate_fee
steps:
- calculate_fee
calculate_fee:
REASONING: false
SET: total_fee = "$25"
RESPOND: "The estimated change fee is $25."
THEN: complete
COMPLETE:
- WHEN: total_fee IS SET
RESPOND: "The estimated change fee is {{total_fee}}."
```
## Verification
1. Validate all files in the project together, not only the supervisor.
2. Send an account access utterance and confirm the selected tool or trace shows `handoff_to_Account_Support_Agent`.
3. Send an order-status utterance and confirm ownership moves to `Order_Status_Agent`.
4. Send "I need a person" or an unclear request and confirm the fallback route reaches `Live_Agent`.
5. For the delegate example, send "What would it cost to change this booking?" and confirm the parent stays active while `Fee_Calculator` returns `total_fee`.
## Production readiness checklist
* Every `HANDOFF TO` and `DELEGATE TO` target exists in the project or is configured as an external/remote agent.
* Every `PASS` field is gathered, read from memory, produced by a tool, or supplied by the channel before it is needed.
* Each route has a distinct business owner and success criteria.
* A fallback or human route exists for unclear, unsupported, or high-risk requests.
* Test utterances cover every route, delegate failure, and human escalation.
* Trace/debug review confirms selected route, target agent, return behavior, and context passed.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Splitting too early | A broad domain looks complex, but one owner can still handle it. | Split only when ownership, tools, policy, or risk differs. |
| Listing agents without routing | `AGENTS:` looks like it connects agents. | Add `HANDOFF` rules for executable transfer. |
| Passing fields with no source | The route needs context that was never gathered or produced. | Gather, remember, or tool-populate every passed field. |
| Using handoff for a subtask result | The parent only needed a calculation or answer. | Use `DELEGATE` when the parent should keep the conversation. |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| The supervisor stays active | No handoff rule was selected. | Check `HANDOFF`, route conditions, intent categories, and fallback path. |
| A target cannot use passed context | The field is missing or named differently. | Check `PASS` field names and target-agent gathered fields. |
| A delegate return is empty or warned | The child does not obviously produce the returned field. | Declare, gather, set, or tool-produce the child return field. |
# How to clarify before routing to a specialist agent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/clarify-before-routing
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this when a request is short, ambiguous, overloaded, or missing the one field that determines ownership.
## Concept
Clarification is the right pattern when the supervisor has too little information to select a specialist. Instead of guessing, gather one routing field with a global `GATHER`, then use deterministic `HANDOFF` conditions over the clarified answer. Keep a quoted semantic fallback for answers that don't cleanly match any expected value.
One thing to get right when you write that fallback condition: keep it in ordinary lowercase sentence form. The runtime's classifier that decides whether a quoted `WHEN` string is natural language or a structured expression checks for `AND`/`OR`/`NOT` **case-sensitively** (uppercase only) in the step that ultimately matters, but an earlier quote-unwrapping check matches `and`/`or` case-**in**sensitively. In practice this means:
* Lowercase connector words in an ordinary sentence (like "the clarified route topic is unsupported **or** still unclear") work correctly and are treated as natural language.
* The same sentence with an uppercase `OR`/`AND`/`NOT` can be misclassified as a structured deterministic expression instead — and since ordinary English words aren't valid variable names, that misclassification will fail rather than degrade gracefully.
Write your fallback conditions the way this example does — as a plain, lowercase sentence — and you won't hit this.
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Clarify_Before_Routing_Supervisor
GOAL: "Collect one routing decision before selecting a specialist"
GATHER:
route_topic:
TYPE: string
PROMPT: "What do you need help with: billing, technical support, cancellation, or something else?"
REQUIRED: true
HANDOFF:
- TO: Billing_Agent
WHEN: route_topic == "billing"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, route_topic]
summary: "Customer clarified that this is a billing request."
- TO: Technical_Support_Agent
WHEN: route_topic == "technical support"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, route_topic]
summary: "Customer clarified that this is a technical support request."
- TO: General_Service_Agent
WHEN: "the clarified route topic is unsupported or still unclear"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, route_topic]
summary: "Customer still needs triage."
AGENT: Billing_Agent
GOAL: "Resolve billing requests"
AGENT: Technical_Support_Agent
GOAL: "Resolve technical support requests"
AGENT: General_Service_Agent
GOAL: "Clarify and route unsupported requests"
```
`customer_id` is passed as context with no declared source in this example — treat it as a project-local assumption (typically populated from authentication) and declare it via `MEMORY` in your actual project.
## How it works
* The top-level `GATHER` runs before any `HANDOFF` is evaluated — the supervisor asks its one clarification question and waits for `route_topic` before routing.
* The two deterministic routes compare `route_topic` against expected values directly. If `route_topic` somehow weren't set yet, the comparison would simply evaluate `false` — no error, and the trace shows your literal authored condition text.
* The fallback route's quoted condition is evaluated by the runtime's semantic-routing classifier, not a deterministic expression evaluator — which is exactly why the lowercase-wording rule above matters.
* Since `HISTORY` is omitted from every `HANDOFF`, each specialist now receives the full conversation history by default (the current platform default).
## Common variations
* Global gather before any specialist route (shown above).
* A fallback agent gathers clarification itself and returns to the supervisor (see the fallback-routing HowTo) instead of resolving directly in the supervisor's own `GATHER`.
* Multi-intent disambiguation when several valid routes are detected in one message, instead of a single missing-field clarification.
## Verification
* Parse and compile the ABL and confirm there are no parser or compiler errors/warnings.
* Test at least one matching utterance for each specialist route, plus an utterance that should hit the fallback (something outside billing/technical support).
* Deliberately test with an answer containing the word "or"/"and" in lowercase to confirm the fallback still classifies correctly; avoid ever writing an uppercase `AND`/`OR`/`NOT` into a quoted fallback condition.
* Inspect the trace for the selected target and the evaluated condition (matches your literal authored text for the deterministic routes).
## Production readiness checklist
* The gathered field asks for exactly the one value needed to route — nothing more.
* Deterministic conditions use declared, gathered, tool-result, runtime, or returned fields.
* Semantic fallback conditions are quoted, written as ordinary lowercase sentences, and never contain a capitalized `AND`/`OR`/`NOT`.
* Fallback behavior is explicit and does not hide missing intent coverage.
* Any variable assumed to already exist (like `customer_id` above) has a real declared source in your project.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Asking for a long form when one routing question is enough | Over-collecting "just in case" | Gather only the field that actually determines ownership |
| Clarifying again after handoff when the supervisor already owns routing | Duplicated logic between supervisor and child | Keep clarification at the level that owns the routing decision |
| Forgetting a fallback for answers outside the expected values | Assuming users will match one of the listed options exactly | Always include a quoted semantic fallback route |
| Writing an uppercase `AND`/`OR`/`NOT` inside a quoted fallback condition | Reads naturally to a human author | Keep quoted fallback text in ordinary lowercase sentence form |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| The fallback route behaves unpredictably or seems to error | Uppercase `AND`/`OR`/`NOT` inside the quoted fallback text got misclassified as a structured expression | Rewrite the fallback condition in ordinary lowercase sentence form |
| The supervisor never routes even after the user answers the clarification question | `route_topic` value doesn't exactly match any deterministic condition's literal string | Check the actual gathered value against the exact strings compared in `WHEN` |
| Child agent has more/less conversation context than expected | `HISTORY` omitted; current default (`full`) applied | Set an explicit `HISTORY` strategy if bounded/summary history is intended |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route unclear requests to a fallback agent](/agent-platform/abl/how-to/route-to-fallback-agent).
* [How to handle messages that contain multiple user intents](/agent-platform/abl/how-to/handle-multiple-intents).
# How to combine reasoning steps with deterministic FLOW steps
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/combine-reasoning-and-flow-steps
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this pattern when a conversation needs both flexible judgment and predictable execution. A good enterprise flow often has one or two places where the agent should reason, such as understanding urgency or explaining a result, surrounded by deterministic steps that collect data, set variables, call tools, branch, and finish reliably.
## Concept
ABL treats reasoning and deterministic behavior as step-level choices inside `FLOW`. `REASONING: true` gives a step a goal-driven reasoning zone. `REASONING: false` makes the step scripted: it should use explicit actions such as `GATHER`, `SET`, `CALL`, `RESPOND`, `ON_INPUT`, `ON_RESULT`, and `THEN`.
Use reasoning steps for judgment: interpreting messy user language, choosing a tone, deciding whether a situation is urgent, or explaining a result. Use deterministic steps for control: collecting required fields, setting session values, invoking tools, branching on known outputs, and ending the flow.
Do not use the old `MODE:` pattern. Current ABL expects `REASONING: true` or `REASONING: false` on each `FLOW` step. A deterministic step with a `GOAL` or `AVAILABLE_TOOLS` is misleading because those only affect reasoning behavior.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Claim_Intake_Agent
GOAL: "Collect claim details, reason about urgency, and route to the right next step"
PERSONA: "Careful claims intake specialist"
FLOW:
entry_point: collect_claim
collect_claim:
REASONING: false
GATHER:
- policy_id:
prompt: "What is your policy ID?"
type: string
required: true
- claim_type:
prompt: "What type of claim are you filing?"
type: string
required: true
THEN: assess_urgency
assess_urgency:
REASONING: true
GOAL: "Decide whether the claim sounds urgent, routine, or unclear. Ask one brief clarification question if needed."
THEN: prepare_summary
prepare_summary:
REASONING: false
SET:
claim_summary = "Policy " + policy_id + " reported a " + claim_type + " claim"
THEN: explain_next_steps
explain_next_steps:
REASONING: true
GOAL: "Explain the next steps in plain language using policy_id, claim_type, and claim_summary."
THEN: COMPLETE
```
## How it works
The first step is deterministic because the platform should collect `policy_id` and `claim_type` before doing anything else. The second step is reasoning because urgency is not always a clean enum: the user may describe an accident, water damage, loss, injury, or uncertainty in their own words. The third step is deterministic again because it constructs a known variable. The final reasoning step turns the known state into a user-facing explanation.
This structure makes the flow easier to test. You can verify that required fields are collected before `assess_urgency`, that `claim_summary` is set before `explain_next_steps`, and that completion happens only after the explanation step.
## Common variations
### Reason first, then collect
Start with a reasoning step when users may open with broad language like "I need help with something that happened yesterday." The reasoning step can decide what to ask next, then transition into deterministic collection.
### Collect first, then reason
Start with deterministic collection when compliance, eligibility, identity, or required intake fields must exist before any judgment is useful.
### Reason after a tool result
Use a deterministic `CALL` and `ON_RESULT` to fetch known data, then use a reasoning step to explain the result. This keeps tool execution predictable while allowing the final response to adapt to the user.
## Verification
* Validate the project and confirm every `FLOW` step declares `REASONING: true` or `REASONING: false`.
* Test a routine claim, an urgent claim, and an unclear claim.
* Confirm `collect_claim` gathers both fields before `assess_urgency`.
* Confirm `prepare_summary` sets `claim_summary` before `explain_next_steps`.
* Inspect the trace for step enter and exit events so you can prove the flow moved through deterministic and reasoning steps in the expected order.
## Production readiness checklist
* Keep deterministic steps free of unused goals and available-tool lists.
* Keep reasoning goals narrow enough that the model knows what decision or explanation it owns.
* Add fallback paths for unclear user input, failed tool calls, and exhausted attempts.
* Use deterministic steps for required data, compliance checks, and side-effecting actions.
* Use traces to monitor unexpected loops, skipped steps, and repeated clarification turns.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using `MODE:` for the whole agent | Older examples may show agent-level execution mode | Declare `REASONING` on each flow step |
| Putting a `GOAL` on a deterministic step | The step looks like it needs explanation | Move the goal to a reasoning step or remove it |
| Making every step reasoning-enabled | Flexible behavior feels attractive during design | Use deterministic steps for control, data movement, tools, and branches |
| Making every step deterministic | The flow becomes brittle for messy language | Add reasoning zones where interpretation or explanation is required |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| The validator says a step must declare reasoning | A `FLOW` step is missing `REASONING` | Add `REASONING: true` or `REASONING: false` to every step |
| A deterministic step does not use its goal | `REASONING: false` ignores step goals | Move the goal to a reasoning step |
| The flow skips useful judgment | A transition jumps around the reasoning step | Check `THEN`, `ON_INPUT`, and `ON_RESULT` branches |
| The agent loops in explanation | The reasoning goal is too broad or has no exit | Narrow the goal and transition to `COMPLETE` |
***
**Related articles:**
* [How to choose between a reasoning agent and a FLOW-based agent](/agent-platform/abl/how-to/choose-reasoning-agent-or-flow).
# How to configure EXECUTION.pipeline models, modes, thresholds, and fallbacks
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/configure-agent-execution-pipeline
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this guide when the agent execution pipeline exists but needs tuning for model choice, mode, confidence thresholds, tool filtering, keyword veto, intent bridging, and fallback behavior.
## Concept
`EXECUTION.pipeline` configuration is a set of routing controls. It does not replace the supervisor's handoff rules. Instead, it helps the runtime decide how aggressively to use classification before the normal reasoning path.
The effective pipeline configuration is resolved from agent-level settings, project-level settings, and defaults. Agent settings can override project settings for the same field. Project settings can fill fields the agent does not specify. Defaults fill the rest. The pipeline is disabled by default, and an agent-level opt-out remains a veto even when the project is enabled.
## Minimal working example
Use this when you only need to enable the execution pipeline and keep the first deployment conservative.
```yaml theme={null}
AGENT: Pipeline_Minimal_Agent
GOAL: "Answer support questions with execution pipeline assistance"
EXECUTION:
pipeline:
enabled: true
mode: sequential
shortCircuit:
enabled: true
confidenceThreshold: 0.9
COMPLETE:
- WHEN: true
RESPOND: "I can help with your support request."
```
This example enables the pipeline, keeps mode predictable with `sequential`, and allows short-circuiting only at high confidence.
## Full configuration example
Treat this as a separate project-level routing example from the minimal agent above.
```yaml expandable=true theme={null}
SUPERVISOR: Insurance_Router
GOAL: "Route insurance requests with tuned classifier thresholds and fallbacks"
PERSONA: "Careful insurance routing supervisor"
EXECUTION:
pipeline:
enabled: true
mode: parallel
model: "gpt-4.1-mini"
shortCircuit:
enabled: true
confidenceThreshold: 0.92
toolFilter:
enabled: true
maxTools: 3
keywordVeto:
enabled: true
keywords: ["appeal", "fraud", "legal"]
intentBridge:
enabled: true
programmaticThreshold: 0.88
guidedThreshold: 0.6
outOfScopeDecline: false
multiIntentSignal: true
HANDOFF:
- TO: Claims_Agent
WHEN: "The user asks to file, check, or update a claim"
SUMMARY: "User needs claims support"
- TO: Benefits_Agent
WHEN: "The user asks about coverage, eligibility, benefits, or plan rules"
SUMMARY: "User needs benefits support"
- TO: Escalation_Agent
WHEN: "The user mentions appeals, fraud, legal risk, or a complaint"
SUMMARY: "User may need escalation review"
```
```yaml theme={null}
AGENT: Claims_Agent
GOAL: "Help members with claim filing and claim status"
PERSONA: "Claims support specialist"
```
```yaml theme={null}
AGENT: Benefits_Agent
GOAL: "Explain plan benefits, coverage, and eligibility"
PERSONA: "Benefits specialist"
```
```yaml theme={null}
AGENT: Escalation_Agent
GOAL: "Handle escalations, complaints, appeals, and risk-sensitive requests"
PERSONA: "Escalation specialist"
```
## How it works
`mode` controls how the pipeline work is organized. Use `sequential` when you want simpler, more predictable behavior. Use `parallel` when your deployment is tuned for faster classification and filtering paths.
`model` chooses the classifier model. Prefer a model that is strong enough for category decisions but inexpensive enough for every turn where the pipeline runs.
`shortCircuit.confidenceThreshold` controls when a single classified intent can bypass broader reasoning. Higher thresholds reduce misroutes but may send more traffic to guided reasoning.
`toolFilter.maxTools` limits how many tools remain visible after filtering. Use it when the agent has many tools or routing tools and the classifier can narrow the relevant set.
`keywordVeto.keywords` prevents short-circuiting when sensitive words appear. These words should reflect business risk, not just common nouns.
`intentBridge.programmaticThreshold` and `intentBridge.guidedThreshold` split routing into deterministic, guided, and autonomous behavior. Defaults exist, but production values should be tuned from real traffic.
Since none of the `HANDOFF` entries in the full configuration example declare `HISTORY`, each specialist now receives the full conversation history by default (the current platform default when `HISTORY` is omitted).
## Common variations
| Variation | Use when | Configuration focus |
| - | - | - |
| Conservative launch | Misroutes are expensive or the category set is new. | `mode: sequential`, high `confidenceThreshold`, keyword veto enabled. |
| Low-latency routing | You have measured latency budget and strong monitoring. | `mode: parallel`, small tool set, tuned thresholds. |
| Classifier-assisted but not autonomous | The classifier should guide reasoning but not route alone. | Keep `guidedThreshold` lower than `programmaticThreshold`. |
| Sensitive-topic routing | Legal, fraud, cancellation, refund, or complaint paths need extra review. | Add `keywordVeto.keywords` and escalation routes. |
| Agent opt-out | Project enables the pipeline but one agent is not ready. | Set agent-level `pipeline.enabled: false` and verify effective config. |
## Configuration checklist
| Setting | Start with | Tighten when |
| - | - | - |
| `mode` | `sequential` | You have latency evidence and monitoring for `parallel` |
| `shortCircuit.confidenceThreshold` | High, such as `0.9` or above | Misroutes are more expensive than slower routing |
| `programmaticThreshold` | Higher than guided threshold | Direct routing or decline must be conservative |
| `guidedThreshold` | Middle confidence range | You want classifier help without full automation |
| `keywordVeto.keywords` | Sensitive terms and side-effect terms | Compliance, refund, legal, fraud, or cancellation risk grows |
| `toolFilter.maxTools` | Small enough to reduce noise | Tool lists become large or overlapping |
## Verification
* Validate the ABL and confirm the pipeline block compiles into execution settings.
* Compare effective config when only project settings exist, only agent settings exist, and both exist.
* Test confidence just below and at `guidedThreshold`.
* Test confidence just below and at `programmaticThreshold`.
* Test `keywordVeto` with each sensitive keyword.
* Test agent opt-out when the project pipeline is enabled.
* Inspect traces for selected mode, classifier model, thresholds, veto, and tiered action.
## Production readiness checklist
* Project-level and agent-level pipeline settings are documented together.
* Thresholds are chosen from real utterance samples and reviewed after launch.
* Escalation or fallback exists for sensitive and ambiguous categories.
* Keyword veto terms are reviewed for false positives and false negatives.
* Tool filtering does not hide tools needed for safe recovery.
* Dashboards track tier distribution, short-circuit rate, guided rate, autonomous rate, and fallback rate.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Lowering programmatic threshold too far | It increases automation | Keep direct routing conservative until measured |
| Using keyword veto as a routing rule | Veto only blocks short-circuiting | Keep routing logic in handoff rules and categories |
| Assuming agent opt-in is enough | Runtime has project-level gate behavior | Verify effective config in the deployed project |
| Filtering tools too aggressively | Smaller tool lists look cleaner | Test failure recovery and edge cases |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Agent config is ignored | Project pipeline is disabled or absent | Compare agent, project, and effective config |
| Guided behavior happens too often | Programmatic threshold is too high or categories are weak | Review classifier confidence and category distinctness |
| Direct routing happens too often | Thresholds are too low or veto terms are missing | Raise thresholds and add sensitive veto words |
| Tool choice gets worse | `toolFilter.maxTools` is too small | Increase max tools or improve tool descriptions |
***
**Related articles:**
* [How to use EXECUTION.pipeline for agent routing, classification, and short-circuiting](/agent-platform/abl/how-to/use-agent-execution-pipeline).
# How to configure model, timeout, and execution settings for an agent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/configure-agent-execution-settings
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use `EXECUTION` when an agent needs explicit model choice, response limits, timeout boundaries, concurrency behavior, thinking settings, history windowing, voice settings, or execution-pipeline configuration.
## Concept
`EXECUTION` is the agent-level configuration surface for runtime behavior. It does not decide whether a flow step is reasoning or deterministic; that is handled by step-level `REASONING`. Instead, `EXECUTION` controls the environment in which the agent runs: which model to use, how long LLM and tool calls can wait, how many iterations are allowed, how much history is retained, and how concurrent messages are handled.
Use explicit settings when the default behavior is not enough for cost, latency, safety, or operational predictability. Keep settings minimal for simple agents. Add settings only when you can explain why the value exists and how it will be tested.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Enterprise_Support_Agent
GOAL: "Answer customer support questions with bounded model and execution behavior"
PERSONA: "Concise support assistant"
EXECUTION:
model: gpt-4.1-mini
temperature: 0.2
max_tokens: 1200
tool_timeout: 8000
llm_timeout: 15000
session_idle_timeout: 1800000
max_reasoning_iterations: 4
max_flow_iterations: 12
concurrency: preemptive
maxQueueDepth: 20
maxConcurrentMessages: 3
enable_thinking: true
thinking_budget: 1024
conversation_history_window: 12
fallback_model: gpt-4.1-mini
```
## How it works
The parser accepts both snake\_case and some camelCase forms for several execution settings. The compiler lowers the values into the agent execution IR. Runtime components then use that execution contract to bound model calls, tool calls, history, concurrency, and related behavior.
The most common settings are:
| Setting | Use it for |
| - | - |
| `model` | Primary model for the agent |
| `temperature` | Response variability |
| `max_tokens` | Output budget |
| `tool_timeout` | Maximum wait for tool calls |
| `llm_timeout` | Maximum wait for LLM calls |
| `session_idle_timeout` | Idle session boundary |
| `max_reasoning_iterations` | Preventing reasoning loops |
| `max_flow_iterations` | Preventing flow loops |
| `concurrency` | Serial, preemptive, or parallel handling strategy |
| `conversation_history_window` | Number of recent turns to retain |
| `fallback_model` | Backup model when fallback is configured |
## Common variations
### Low-latency support agent
Use a smaller model, low token budget, short LLM timeout, and small history window when the agent handles high-volume support questions.
### Careful enterprise agent
Use lower temperature, bounded reasoning iterations, and explicit timeout values when the agent answers regulated, contractual, or account-related questions.
### Voice or realtime agent
Use voice-specific execution settings and stricter latency budgets. Test the experience in the voice channel because a setting that works in chat may feel slow on a call.
### Agent execution pipeline
Use nested `EXECUTION.pipeline` only when you need agent routing, classification, short-circuiting, tool filtering, keyword veto, or intent bridging. That is a separate decision from choosing a base model or timeout.
## Verification
* Validate the agent and confirm `EXECUTION` compiles into the agent execution settings.
* Test a normal model response and a slow model response against `llm_timeout`.
* Test a normal tool call and a slow tool call against `tool_timeout`.
* Inspect traces for model, timeout, and execution-path metadata.
* Load test concurrency settings before using `parallel` or high queue depths in production.
## Production readiness checklist
* Every non-default setting has an owner and reason.
* Timeout values match the channel experience and downstream service SLOs.
* Token limits are high enough for expected answers but low enough for cost control.
* Iteration limits prevent loops without cutting off valid flows.
* Fallback behavior is tested, not just configured.
* Voice settings are tested with real turn-taking and latency expectations.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using top-level `MODEL:` | It looks natural as a section | Put `model` under `EXECUTION` |
| Using `MODE:` for reasoning/scripted behavior | Older patterns used agent-level mode | Use step-level `REASONING` in `FLOW` |
| Setting timeouts too high | Avoids visible failures in testing | Match timeouts to user experience and service SLOs |
| Copying settings across agents | It is faster during setup | Tune by agent role, channel, and risk |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Parser rejects `MODEL:` | Model is not a top-level section | Move it to `EXECUTION.model` |
| Agent is slow under load | Timeout or concurrency settings are too loose | Check traces, queue depth, and downstream service latency |
| Agent stops mid-flow | Iteration limit is too low | Inspect step count and loop behavior |
| Costs are higher than expected | Model, token, or history settings are too large | Review `model`, `max_tokens`, and history window |
***
**Related articles:**
* [How to use EXECUTION.pipeline for agent routing, classification, and short-circuiting](/agent-platform/abl/how-to/use-agent-execution-pipeline).
* [How to combine reasoning steps with deterministic FLOW steps](/agent-platform/abl/how-to/combine-reasoning-and-flow-steps).
# How to create a welcome message for a new conversation
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/create-welcome-message
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when a conversation should open with a clear first message before the user asks for help.
## Concept
A welcome message is the first assistant-owned turn in a session. In ABL, the most direct way to author it is `ON_START`, because `ON_START` runs when the runtime initializes the session, before the first user message is processed.
Use a welcome message to orient the user, name the service, and make the first action obvious. Keep it short. If the same agent also has a `FLOW`, the top-level `ON_START` response behaves like a startup prelude: it can be emitted and the entry flow can continue. If an `ON_START` branch is selected, that branch owns the startup turn and the runtime can pause before the flow continues.
## Minimal working example
```yaml theme={null}
AGENT: Welcome_Message_Agent
GOAL: "Welcome the user and then continue to the first support step"
ON_START:
RESPOND: "Welcome to Acme Support. I can help with orders, returns, billing, or account questions."
FLOW:
entry_point: listen
steps:
- listen
listen:
REASONING: false
RESPOND: "What would you like help with today?"
THEN: COMPLETE
```
## How it works
The runtime initializes the session, executes `ON_START`, emits the authored response when the channel supports proactive startup delivery, and then enters the flow entry step. The welcome response is also written into assistant history when it is delivered, so later reasoning can see what the user has already been told.
The first flow response should not repeat the welcome. Use it to ask the first useful question or show the first menu.
## Common variations
### Add buttons or rich content
Use actions when the channel can render buttons and you want the user to choose a common path immediately.
```yaml expandable=true theme={null}
AGENT: Rich_Welcome_Agent
GOAL: "Welcome users with a card and a clear first action"
ON_START:
RESPOND: "Welcome to Acme Support."
FORMATS:
markdown: |
## Welcome to Acme Support
Choose a topic to begin.
ACTIONS:
- BUTTON: "Track an order" -> track_order
VALUE: "track_order"
- BUTTON: "Start a return" -> start_return
VALUE: "start_return"
FLOW:
entry_point: wait_for_choice
steps:
- wait_for_choice
wait_for_choice:
REASONING: false
RESPOND: "Select an option or describe your question."
THEN: COMPLETE
```
## Verification
Start a new session and confirm that the welcome appears before the first user message. In traces, look for `dsl_on_start` followed by `dsl_respond` with `source: on_start`. If the flow also runs, the flow response should appear after the welcome.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Repeating the same greeting in `ON_START` and the first flow step | Both are visible during startup on proactive channels. | Make `ON_START` orient the user and make the first flow step ask the next useful question. |
| Putting long policy text in the welcome | The welcome becomes a wall of text before the user asks anything. | Keep startup copy short and move details into the next guided step. |
| Assuming every channel shows the proactive welcome | Some channel contracts suppress startup payloads. | Test each channel and keep critical initialization separate from visible copy. |
## Troubleshooting
If the welcome does not appear, check the channel contract. Some channels suppress proactive startup payloads and allow `ON_START` only for side effects. If the flow response repeats the welcome, move the greeting into `ON_START` and make the flow step ask the next question.
## Production readiness checklist
* Keep the welcome short enough for every channel.
* Include a useful next step, not only a greeting.
* Use rich content only when the target channel supports it.
* Verify traces for `dsl_on_start` and `dsl_respond`.
* Confirm the first flow step does not duplicate the startup message.
# How to debug routing decisions in traces
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/debug-routing-traces
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this when support, QA, or partners need to understand why a user went to one specialist instead of another, or why a route silently fell through to fallback.
## Concept
Every `HANDOFF` decision, whether deterministic (`WHEN: intent.category == "billing"`) or semantic (`WHEN: "the request is about billing"`), produces a trace event describing what was evaluated and why a target was or wasn't selected. The runtime does not just log "routed to Billing\_Agent" — it records the condition it evaluated, the context available at evaluation time, the outcome, and (for temporary child agents) what came back through `ON_RETURN`.
One thing surprises people the first time they read a routing trace: **not every routing event is visible at every trace verbosity level.** The runtime supports `minimal`, `standard`, `verbose`, and `debug` verbosity. If you're not seeing an event you expect (like `handoff_condition_check` for a route that *didn't* match), raise verbosity before assuming the event doesn't exist.
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Traceable_Routing_Supervisor
GOAL: "Make routing decisions easy to inspect in traces"
ON_START:
SET:
_meta.entry_channel = session.channel
_meta.routing_version = "support-router-v3"
INTENTS:
billing: "Billing and payment requests."
technical_support: "Product troubleshooting requests."
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, account_id, conversation_summary]
summary: "Traceable billing route."
- TO: Technical_Support_Agent
WHEN: intent.category == "technical_support"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, issue_summary, conversation_summary]
summary: "Traceable technical support route."
- TO: Fallback_Triage_Agent
WHEN: "the request does not clearly match billing or technical support"
EXPECT_RETURN: true
CONTEXT:
pass: [customer_id, conversation_summary]
summary: "Traceable fallback route."
ON_RETURN:
action: resume_intent
AGENT: Billing_Agent
GOAL: "Resolve billing requests"
AGENT: Technical_Support_Agent
GOAL: "Resolve technical support requests"
AGENT: Fallback_Triage_Agent
GOAL: "Clarify unclear requests"
```
`customer_id`, `account_id`, `issue_summary`, and `conversation_summary` are shown as passed context fields; declare them via `MEMORY` or populate them via `GATHER`/tool results in your actual project — this example assumes they already exist as project-local session values.
## How it works
* The supervisor keeps ownership until a `HANDOFF` condition matches, evaluated in authored order.
* Each evaluated condition emits `handoff_condition_check` with a shape similar to:
```json theme={null}
{
"event": "handoff_condition_check",
"condition": "intent.category == \"billing\"",
"result": false,
"evaluatedContext": { "intent.category": "technical_support" },
"target": "Billing_Agent",
"whenOutcome": "deterministic_no_match"
}
```
For a matched deterministic route, the runtime also emits `deterministic_routing` and `deterministic_handoff`; for a matched semantic (quoted natural-language) `WHEN`, `whenOutcome` reflects the semantic evaluation path instead.
* If a condition references a variable that is not yet set anywhere in scope, the runtime emits `route_condition_unresolved` instead of silently treating it as a non-match — check for this event before concluding "the condition just didn't match."
* If a flow-level return suppresses a condition that would otherwise have matched (for example, the conversation is mid-flow and routing is intentionally deferred), the runtime emits `handoff_condition_suppressed`.
* Once a target is selected, `handoff_context_pass_resolved` records what the resolved `CONTEXT.pass` values actually were, and `handoff_context_set_applied` records any `CONTEXT.set` writes applied to the child.
* When `EXPECT_RETURN: true` is used (as in the fallback route above), the child's return path emits `handoff_return_handler`, and `ON_RETURN.action: resume_intent` produces a `resume_intent` trace entry showing what intent processing resumed with.
* Remember: since `HISTORY` is omitted from every `HANDOFF` above, each child receives the full conversation history by default (the platform default when `HISTORY` is unset). Set `HISTORY: auto` or another explicit strategy if you want bounded/summary history instead, and expect the trace's history-related fields to reflect whichever strategy is in effect.
* `_meta.*` values (like `_meta.entry_channel` above) are not routing logic — they're custom trace dimensions for filtering/grouping in dashboards. You can `SET _meta.*` in `ON_START`, in flow steps, in lifecycle hooks, and in `ON_ERROR`/`ESCALATE` triggers, not only at conversation start.
## Common variations
### Debugging a semantic (natural-language) condition
Quoted `WHEN` text like `WHEN: "the request does not clearly match billing or technical support"` produces the same `handoff_condition_check` event, but `whenOutcome` distinguishes the semantic evaluation path from a deterministic one. There's no separate event name to search for — filter on `whenOutcome` instead.
### Debugging multiple intents in one message
Multi-intent routing emits its own event family instead of a single glob: `multi_intent_plan_built` (the queue was constructed), `multi_intent_target_resolved` (a queue entry resolved to a target agent), `multi_intent_queued` (an entry queued for later processing), `multi_intent_sequential` plus `multi_intent_sequential_task_start`/`_task_complete`/`_executed` (sequential processing), and `multi_intent_parallel` (parallel processing). Search for the specific event you expect rather than a wildcard.
### Debugging trace visibility
If an event you expect isn't showing up, check the trace verbosity setting first (`minimal`/`standard`/`verbose`/`debug`) before assuming the routing logic is wrong — some events only appear at `verbose` or `debug`.
## Verification
* Parse and compile the ABL and confirm there are no parser or compiler errors/warnings.
* Test at least one matching utterance for each deterministic route, one for the semantic fallback, and one that should trigger `route_condition_unresolved` (reference a variable you know is unset).
* Inspect the trace for: the selected `target`, the `condition` actually evaluated (matches your literal authored text), `evaluatedContext`, and — for `EXPECT_RETURN: true` routes — the `handoff_return_handler` and `resume_intent` entries.
* Raise trace verbosity to `verbose` or `debug` if an expected event isn't appearing.
## Production readiness checklist
* Every specialist route has a clear owner and a concise context summary.
* Deterministic conditions use declared, gathered, tool-result, runtime, or returned fields.
* Semantic conditions are quoted as natural-language `WHEN` text.
* Temporary child agents (`EXPECT_RETURN: true`) produce every field mapped in `ON_RETURN`, verified via `handoff_return_handler`/`resume_intent` traces.
* Fallback behavior is explicit and does not hide missing intent coverage — confirm via `route_condition_unresolved` and `handoff_condition_suppressed` that nothing is silently falling through.
* `_meta.*` dimensions are stable, low-cardinality values (not raw user text), set consistently across the surfaces where routing decisions can occur (not just `ON_START`).
* Trace verbosity is set appropriately for the environment (higher in staging/debugging, tuned for volume/cost in production).
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Only checking `handoff_condition_check` for the matched route | Skipped candidates are just as diagnostic when a route unexpectedly loses to another | Inspect all `handoff_condition_check` entries in the turn, not just the one that matched |
| Setting `_meta.*` only in `ON_START` | The article/example historically only showed `ON_START` | Also set `_meta.*` in flow steps, hooks, or `ON_ERROR`/`ESCALATE` when the dimension should reflect later-turn state |
| Setting high-cardinality `_meta.*` values (for example, raw user utterance) | Seems convenient for debugging one session | Use stable, low-cardinality values; high-cardinality dimensions break dashboard aggregation |
| Not finding an expected trace event | Trace verbosity is below the level that surfaces it | Raise verbosity to `verbose`/`debug` before concluding the event doesn't fire |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| A route you expect to match never fires, and there's no `handoff_condition_check` for it at all | Trace verbosity is too low, or the flow suppressed the condition before it was ever evaluated | Raise verbosity; check for a `handoff_condition_suppressed` entry |
| A condition evaluates to a non-match even though the variable looks set in your test | The variable is a different path than the condition references, or it's unset at evaluation time | Look for `route_condition_unresolved`; confirm the variable's actual path via `evaluatedContext` in `handoff_condition_check` |
| A temporary child agent's fields never make it back to the parent | Missing or mismatched `ON_RETURN` field mapping | Inspect `handoff_return_handler` for the actual returned payload shape |
| Child agent has less/more conversation context than expected | `HISTORY` was omitted and the platform default (`full`) applied, or an explicit strategy elsewhere in the project differs from what you assumed | Check `HISTORY`/`CONTEXT.history` on the `HANDOFF`; confirm against the current platform default |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route conversations based on user intent](/agent-platform/abl/how-to/route-by-user-intent).
* [How to handle messages that contain multiple user intents](/agent-platform/abl/how-to/handle-multiple-intents).
# How to define clear responsibilities for each enterprise agent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/define-agent-responsibilities
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this guide when an agent is becoming too broad, too vague, or difficult for customers, builders, and support teams to reason about.
## Concept
An agent responsibility is a contract. It says what the agent owns, what it can do, what it must not do, what information it needs, and when it should stop, hand off, or escalate.
Clear responsibilities make routing safer. A supervisor can only route well when every specialist has a distinct job.
| Surface | What it defines |
| - | - |
| `GOAL` | The business outcome the agent owns. |
| `PERSONA` | How the agent communicates. |
| `LIMITATIONS` | What the agent should not claim or attempt. |
| `GATHER` | Information the agent must collect. |
| `CONSTRAINTS` | Conditions that must be true before proceeding. |
| `TOOLS` | External capabilities the agent can use. |
| `HANDOFF` | When another agent should take ownership. |
| `DELEGATE` | When another agent should provide a result while this agent stays active. |
| `COMPLETE` | When the agent can finish the interaction. |
Limitations guide the model. For hard enforcement, pair them with tools, constraints, guardrails, permissions, and handoff boundaries.
## Decision guide
| Use this design choice | When it fits |
| - | - |
| Keep one agent | One business owner, one data boundary, one tool set, and one success definition cover the work. |
| Split into specialist agents | Different owners, permissions, policies, tools, or completion criteria are involved. |
| Add handoff boundaries | Another agent should own the rest of the customer journey. |
| Add delegate boundaries | The current agent needs a subtask result but should remain the active conversation owner. |
| Add constraints, guardrails, tools | A limitation must be enforced rather than only described. |
| Add human escalation | Verification, safety, policy, or customer frustration requires a person. |
## Responsibility worksheet
Before writing the agent, answer these questions:
| Question | Example answer |
| - | - |
| What business outcome does this agent own? | Recover account access after identity verification. |
| What must it never do? | Bypass verification or answer billing questions. |
| What information must it collect? | `account_id`, `verification_method`. |
| Which tools or data can it use? | Identity verification tools, account recovery tools. |
| When should it hand off? | Billing request, human request, verification cannot continue. |
| How does it complete? | Required identity context is present and the next recovery step is clear. |
## Minimal working example: account-security specialist project
This example is a project-level set. The responsibility boundary is visible in the account-security agent, and the handoff targets are defined in the same project.
```yaml expandable=true theme={null}
AGENT: Account_Security_Agent
GOAL: "Verify identity and help users recover account access"
PERSONA: |
Security-conscious account support specialist.
Explains verification steps clearly.
LIMITATIONS:
- "Cannot bypass identity verification"
- "Cannot answer billing questions"
INTENTS:
billing: "Billing, invoices, payments, or subscription charges"
human_help: "User asks for a person or verification cannot continue"
GATHER:
account_id:
prompt: "What is your account ID?"
type: string
required: true
verification_method:
prompt: "How would you like to verify your identity?"
type: string
required: true
CONSTRAINTS:
- REQUIRE account_id IS SET
ON_FAIL: "I need your account ID before I can continue."
HANDOFF:
- TO: Billing_Support_Agent
WHEN: intent.category == "billing"
PASS: [account_id]
SUMMARY: "Customer needs billing support"
RETURN: false
- TO: Live_Agent
WHEN: intent.category == "human_help"
PASS: [account_id]
SUMMARY: "Customer needs a human agent or cannot continue verification"
RETURN: false
COMPLETE:
- WHEN: account_id IS SET AND verification_method IS SET
RESPOND: "I can continue account recovery for {{account_id}} using {{verification_method}} verification."
```
```yaml theme={null}
AGENT: Billing_Support_Agent
GOAL: "Answer billing questions after account-security handoff"
GATHER:
account_id:
prompt: "What account should I review?"
type: string
required: true
COMPLETE:
- WHEN: account_id IS SET
RESPOND: "I can continue billing support for {{account_id}}."
```
```yaml theme={null}
AGENT: Live_Agent
GOAL: "Transfer customers to a human support specialist"
COMPLETE:
- WHEN: true
RESPOND: "I will connect you with a human support specialist."
```
The account-security agent owns identity verification and recovery. Billing and human assistance are outside that responsibility and are represented as handoff boundaries.
Since neither `HANDOFF` entry above declares `HISTORY`, each target now receives the full conversation history by default (the current platform default when `HISTORY` is omitted). Also, because each `WHEN` here is a single-field comparison (`intent.category == "billing"`), an unset field simply makes the comparison evaluate `false` — no error, and the trace shows your literal source text.
## How to write strong goals
Weak goal: "Help customers with account stuff."
Strong goal: "Verify identity and help users recover account access."
A strong goal names the business outcome, the user, and the scope. It should be narrow enough that an owner can say whether the agent succeeded.
## How to write useful limitations
| Limitation | Why it helps |
| - | - |
| `Cannot bypass identity verification` | Prevents unsafe account recovery. |
| `Cannot approve refunds` | Separates policy explanation from financial action. |
| `Cannot provide medical diagnosis` | Draws a regulated-domain boundary. |
For regulated or high-risk actions, do not rely on limitations alone. Add hard boundaries through tools, constraints, guardrails, permissions, or human handoff.
## When to split an agent
Split an agent when it has more than one business owner, more than one permission model, conflicting limitations, different tools by topic, or different success criteria by request type.
Keep one agent when the same owner, policy, tools, data, and success criteria apply across the conversation.
Use `DELEGATE` instead of splitting when the active agent only needs a subtask result and should keep the user conversation.
## Verification
1. Validate the account-security agent and its handoff targets together.
2. Send: "I need to recover my account." Confirm `account_id` and `verification_method` are gathered.
3. Send: "I have a billing question." Confirm the route goes to `Billing_Support_Agent` and passes `account_id`.
4. Send: "I want to talk to someone." Confirm the route goes to `Live_Agent`.
5. Inspect trace/debug output for selected handoff target, passed context, and final active agent.
## Production readiness checklist
* Each specialist has one primary business owner and one primary outcome.
* Required fields are gathered or otherwise populated before tools, handoffs, or completion need them.
* Sensitive or regulated actions are enforced with tools, constraints, guardrails, permissions, or human handoff.
* Out-of-scope intents have handoff routes.
* Success, out-of-scope, human escalation, and failure paths have test utterances.
* Trace/debug output shows clear ownership transitions.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Writing vague goals | The builder starts from a department name instead of an outcome. | Name the exact business outcome. |
| Treating limitations as hard enforcement | Limitations are easy to write and read. | Add constraints, tools, guardrails, permissions, or handoff for hard boundaries. |
| Creating overlapping specialists | Teams define agents by org chart instead of user outcomes. | Use distinct goals and test ambiguous requests. |
| Handing off without context | The boundary is defined, but the target lacks state. | Pass only the fields the target needs and verify they are populated. |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| A specialist answers outside its area | Goal or limitations are too broad, or no handoff boundary exists. | Tighten `GOAL`, add specific `LIMITATIONS`, and add `HANDOFF` routes. |
| Required fields warn or appear unused | The agent collects fields but never consumes them. | Use the fields in completion, tools, memory, handoff, delegate, or constraints. |
| Supervisor routing is inconsistent | Specialist responsibilities overlap. | Revisit each specialist's owner, outcome, and out-of-scope routes. |
# How to design channel-specific welcome experiences
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/design-channel-specific-welcome
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when web chat, voice, messaging, or async channels need different startup behavior.
## Concept
Channels do not all deliver startup content the same way. Some support proactive `ON_START` messages. Some suppress startup payloads and allow `ON_START` only for side effects such as setting values or delegating. A good channel-specific welcome separates the authored conversation welcome from launcher text, placeholder text, and channel-native connection messages.
In ABL, use startup context and `ON_START BRANCHES` to vary the welcome content. Use channel configuration for launcher copy that appears outside the conversation transcript.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Channel_Welcome_Agent
GOAL: "Adapt welcome copy by channel context"
ON_START:
SET: channel_name = session.channel
BRANCHES:
- IF: channel_name == "voice"
RESPOND: "Thanks for calling. Say billing, orders, or agent."
VOICE:
PLAIN_TEXT: "Thanks for calling. Say billing, orders, or agent."
- ELSE:
RESPOND: "Welcome. You can type billing, orders, or agent."
FORMATS:
markdown: |
**Welcome.** Type a topic to begin.
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "Ready."
THEN: COMPLETE
```
## How it works
The runtime copies the channel name into `channel_name`, evaluates branches in order, and selects the first matching response. The voice branch includes a `VOICE` payload. The non-voice branch includes markdown.
## Common variations
### Keep launcher copy separate
```yaml expandable=true theme={null}
AGENT: Launcher_Copy_Agent
GOAL: "Keep channel launcher copy separate from the ABL startup message"
ON_START:
RESPOND: "Welcome inside the conversation."
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "Ask a question to continue."
THEN: COMPLETE
```
Launcher welcome text, placeholder text, and connecting status text are channel configuration. The ABL `ON_START` response is conversation content.
## Verification
Start sessions for each target channel. Confirm the conversation transcript receives the ABL welcome only on channels that support proactive startup delivery. For suppressed channels, confirm `ON_START` side effects still run and traces include `dsl_on_start_skipped`.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Treating launcher text as the ABL welcome | Both appear near session start in web chat. | Keep launcher copy in channel configuration and conversation copy in `ON_START`. |
| Writing one welcome for every channel | Text chat, voice, and async channels have different delivery constraints. | Branch from channel context and test the deployed channel contract. |
| Putting channel side effects inside branches | `ON_START` branches select responses, not startup side effects. | Run `SET` or `CALL` at top-level `ON_START`, then branch on the resulting values. |
## Troubleshooting
If a welcome appears twice in web chat, check whether the SDK launcher message and ABL `ON_START` response say the same thing. If a voice greeting is too long, move details into the next turn and keep startup speech short.
## Production readiness checklist
* Test each deployed channel separately.
* Keep launcher copy and conversation copy distinct.
* Use `VOICE` payloads for voice-specific phrasing.
* Provide text fallback for rich content.
* Verify suppression behavior for async channels.
# How to design reusable agents and modules for large projects
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/design-reusable-agent-modules
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this pattern when a project has many assistants, specialists, tools, and workflows that need to be composed without duplicating responsibilities or creating routing ambiguity.
## Concept
Reusable design in ABL starts with stable agent boundaries. A reusable agent owns a business capability, has a clear goal, accepts a predictable context package, and can be called from more than one supervisor or entry point.
Do not make reuse mean "one giant shared agent." Reuse works best when specialists are small enough to be reliable but broad enough to represent a durable business capability, such as product advice, order status, billing disputes, identity verification, or appointment scheduling.
For tools, current validation rejects removed `FROM ... USE` tool import syntax in agent DSL. Keep agent files focused on agent behavior and manage tool implementations through project tools and deployment configuration.
## Minimal working example
```yaml theme={null}
SUPERVISOR: Commerce_Supervisor
GOAL: "Route shopping requests to reusable commerce specialists"
PERSONA: "Concise commerce routing supervisor"
HANDOFF:
- TO: Product_Advisor
WHEN: "The user asks for product recommendations, comparisons, or availability"
PASS: [customer_id, locale]
SUMMARY: "User needs shopping advice"
- TO: Order_Status_Agent
WHEN: "The user asks about an existing order, delivery, or return status"
PASS: [customer_id, order_id]
SUMMARY: "User needs order status help"
```
```yaml theme={null}
AGENT: Product_Advisor
GOAL: "Help customers compare products and choose the best option"
PERSONA: "Practical shopping advisor"
```
```yaml theme={null}
AGENT: Order_Status_Agent
GOAL: "Help customers understand order, delivery, and return status"
PERSONA: "Precise order support specialist"
```
## How it works
The supervisor is reusable because it routes to stable capability agents. The specialist agents are reusable because they describe their responsibility without depending on one specific entry point. The `PASS` fields are the integration contract between the supervisor and specialists.
In a large project, keep the reusable boundary visible in three places: the agent goal, the routing condition, and the context package. If those three disagree, reuse becomes fragile.
Since neither `HANDOFF` above declares `HISTORY`, each specialist now receives the full conversation history by default (the current platform default when `HISTORY` is omitted).
## Common variations
### Shared specialist agent
Use one specialist from several supervisors when the business capability is truly the same. For example, `Order_Status_Agent` can serve commerce chat, WhatsApp, and voice entry points if the required context is consistent.
### Channel-specific wrapper
Use a thin channel-specific supervisor when routing or welcome behavior differs by channel, while the underlying specialists remain shared.
### Project manifest composition
Use the project manifest to list the agents, entry agent, and project assets. Keep agent names stable because handoff targets depend on them.
### Shared tool implementations
Manage reusable tool implementations in Project Tools. Agent DSL should declare behavior and use tools through validated current syntax; do not rely on removed `FROM ... USE` imports.
## Verification
* Validate the full project group, not only one agent file.
* Confirm every handoff target exists in the same project.
* Test each reusable specialist from every entry point that can route to it.
* Confirm every `PASS` field has a source before handoff.
* Inspect traces to prove the active agent changes to the intended specialist.
## Production readiness checklist
* Agent names are stable and unique.
* Each reusable agent has one durable business capability.
* Handoff context is minimal, explicit, and privacy-reviewed.
* Shared specialists have tests from each supervisor or channel entry point.
* Tool bindings and permissions are configured outside removed import syntax.
* Ownership is clear for shared modules so one team does not break another team's entry point.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Reusing an agent with a vague goal | It feels flexible | Define a durable business capability |
| Passing too much context | Teams want reuse to be easy | Pass only fields the specialist needs |
| Depending on removed tool import syntax | Older examples may still show it | Use current Project Tools setup |
| Sharing specialists without shared tests | The first entry point works | Test every supervisor and channel that can call the specialist |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Handoff target is not found | Agent name mismatch or missing project asset | Compare `TO:` targets with project agent names |
| Specialist gives entry-point-specific answers | Its goal or persona includes one channel or journey | Rewrite it around the reusable capability |
| A shared change breaks another flow | No owner or compatibility test for the module | Add cross-entry tests before changing shared specialists |
| Tool setup fails after reuse | Tool implementation was assumed in agent DSL | Verify Project Tools bindings and deployment configuration |
***
**Related articles:**
* [How to decide whether to build one agent or multiple specialist agents](/agent-platform/abl/how-to/choose-single-agent-or-specialists).
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
# How to design a supervisor that routes users to specialist agents
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/design-supervisor-routing-agent
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this guide when one conversational entry point must identify what the user needs, choose the right specialist, and pass enough context for that specialist to continue.
## Concept
A supervisor is the front door for a multi-agent experience. Its primary job is not to solve every problem. Its job is to understand the request, choose the right owner, and transfer context.
A specialist agent owns one business capability. A good specialist has a narrow goal, the right tools or data access, clear limitations, and a completion path.
Routing is the decision that connects the user request to the correct owner. In ABL, the executable route is `HANDOFF`. `AGENTS:` is an optional roster that makes the supervisor easier to read, but it does not route by itself.
## Decision guide: routing condition styles
| Style | Example | Use when |
| - | - | - |
| Declared intent category | `WHEN: intent.category == "order_status"` | You want stable route labels for testing and reporting. |
| Structured condition | `WHEN: customer_id IS SET` | The route depends on known fields or variables. |
| Quoted semantic condition | `WHEN: "the user is asking for a refund"` | The route is best described as plain-language guidance. |
Quote natural-language conditions. Unquoted text can be parsed as variable names and produce undefined-variable warnings.
## Minimal working example: support supervisor project
This example is a project-level set. Put the supervisor and target agents in the same project.
`agents/support-supervisor.agent.abl`
```yaml expandable=true theme={null}
SUPERVISOR: Support_Supervisor
GOAL: "Route support requests to the right specialist"
AGENTS:
account: Account_Support_Agent
orders: Order_Status_Agent
human: Live_Agent
INTENTS:
account: "Account access or password help"
order_status: "Order tracking or delivery help"
human_help: "User asks for a person or the request is unclear"
GATHER:
customer_id:
prompt: "What customer ID should I use for this request?"
type: string
required: true
HANDOFF:
- TO: Account_Support_Agent
WHEN: intent.category == "account"
PASS: [customer_id]
SUMMARY: "Customer needs account or password support"
RETURN: true
- TO: Order_Status_Agent
WHEN: intent.category == "order_status"
PASS: [customer_id]
SUMMARY: "Customer needs order status or delivery support"
RETURN: false
- TO: Live_Agent
WHEN: intent.category == "human_help"
PASS: [customer_id]
SUMMARY: "Customer asks for a person or the request is unclear"
RETURN: false
```
`agents/account-support-agent.agent.abl`
```yaml theme={null}
AGENT: Account_Support_Agent
GOAL: "Resolve account access and password support requests"
GATHER:
customer_id:
prompt: "What customer ID should I use?"
type: string
required: true
COMPLETE:
- WHEN: customer_id IS SET
RESPOND: "I can continue account support for {{customer_id}}."
```
`agents/order-status-agent.agent.abl`
```yaml theme={null}
AGENT: Order_Status_Agent
GOAL: "Resolve order status and delivery support requests"
GATHER:
customer_id:
prompt: "What customer ID should I use?"
type: string
required: true
COMPLETE:
- WHEN: customer_id IS SET
RESPOND: "I can continue order support for {{customer_id}}."
```
`agents/live-agent.agent.abl`
```yaml theme={null}
AGENT: Live_Agent
GOAL: "Transfer the customer to a human support queue"
COMPLETE:
- WHEN: true
RESPOND: "I will connect you with a human support specialist."
```
The supervisor gathers `customer_id`, then passes it with `PASS`. `RETURN: true` on account support means bounded specialist work can return to the supervisor. `RETURN: false` on order status and live agent means the target keeps ownership.
Since none of the `HANDOFF` entries above declare `HISTORY`, each specialist now receives the full conversation history by default (the current platform default when `HISTORY` is omitted). Also, because each `WHEN` here is a single-field comparison (`intent.category == "account"`), an unset field simply makes the comparison evaluate `false` — no error, and the trace shows your literal source text.
## Common variations
### Plain-language routing variation
Use quoted semantic conditions when business users can describe a route more clearly in natural language than as a strict expression. Treat this as an alternative supervisor project to the previous example; do not combine both supervisors in the same deployment unless the project explicitly declares the intended entry supervisor.
`agents/billing-supervisor.agent.abl`
```yaml theme={null}
SUPERVISOR: Billing_Supervisor
GOAL: "Route billing questions to the right specialist"
HANDOFF:
- TO: Pending_Payments
WHEN: "user is asking about payment status, unpaid balance, invoice due date, or how to pay"
SUMMARY: "Customer needs help with pending payments or payment instructions"
- TO: Refund_Guidance
WHEN: "user is asking about a refund, reimbursement, returned payment, claim payout, or refund timeline"
SUMMARY: "Customer needs refund guidance or refund status help"
```
`agents/pending-payments.agent.abl`
```yaml theme={null}
AGENT: Pending_Payments
GOAL: "Help customers understand pending payments and payment instructions"
COMPLETE:
- WHEN: true
RESPOND: "I can help with pending payments and payment instructions."
```
`agents/refund-guidance.agent.abl`
```yaml theme={null}
AGENT: Refund_Guidance
GOAL: "Help customers understand refund status and refund next steps"
COMPLETE:
- WHEN: true
RESPOND: "I can help with refund status and refund next steps."
```
Keep semantic routes distinct. If two routes could match the same request, customers may experience inconsistent routing.
## Designing the context package
Every handoff should answer three questions.
| Question | Design guidance |
| - | - |
| What does the target need to know? | Pass only useful fields, such as `customer_id`, `order_id`, `issue_summary`, or `transfer_reason`. |
| Why is the transfer happening? | Use `SUMMARY` so the target starts with a concise reason. |
| Should control come back? | Use `RETURN: true` for bounded specialist work and `RETURN: false` for permanent transfer. |
Do not pass every field by default. Pass the context the target needs to continue safely.
## Fallback and ambiguity design
Do not rely on the roster as a fallback. If no handoff is selected, the supervisor remains the active agent and should clarify, retry, or route to a fallback based on your design.
For production supervisors:
* Add a route for human help, unsupported requests, or unclear requests.
* Make route descriptions mutually exclusive.
* Add test utterances that intentionally look ambiguous.
* Inspect traces to confirm the selected handoff target.
* If your runtime configuration supports multi-intent disambiguation, verify that the user is asked to choose when a message contains multiple requests.
## Verification
1. Validate the supervisor and all target agents together.
2. Send: "I forgot my password." Expect `Account_Support_Agent`.
3. Send: "Where is my order?" Expect `Order_Status_Agent`.
4. Send: "I need to talk to someone." Expect `Live_Agent`.
5. Confirm `customer_id` is gathered before routes that pass it.
6. Inspect trace/debug output for handoff target, return behavior, and context passed.
## Production readiness checklist
* Every `TO` target exists and has a clear owner.
* Every `PASS` field is populated before handoff.
* Every route has a distinct condition and summary.
* A human or fallback route exists.
* Route tests cover success, no-match, ambiguous, and escalation paths.
* Trace/debug output is reviewed for each route before go-live.
* Support owners agree on what `RETURN: true` and `RETURN: false` mean for their customer journey.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Treating `AGENTS:` as routing | The roster looks like configuration. | Add `HANDOFF` entries for executable routes. |
| Passing unavailable context | The route references a field that was never gathered or produced. | Gather it, read it from memory, or remove it from `PASS`. |
| Omitting fallback | Happy-path routing looks complete. | Add human, unsupported, or clarification handling. |
| Overlapping route conditions | Business categories are not mutually exclusive. | Test ambiguous utterances and tighten route descriptions. |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| No transfer happens | No handoff route was selected. | Check `HANDOFF`, `WHEN`, intent category, and fallback coverage. |
| Wrong target receives the user | Route conditions overlap. | Rewrite conditions and add route-specific test utterances. |
| Target asks for information already collected | Context was not passed or field names differ. | Check `PASS`, target `GATHER`, and trace/debug context. |
# How to handle empty-state and fallback greetings
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/handle-fallback-greetings
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when startup context may be missing, a branch may not match, or a channel may not deliver proactive welcome content.
## Concept
A fallback greeting is the safe default that prevents the first turn from feeling broken. Fallbacks are needed because startup conditions can fail, channel metadata can be absent, and some delivery contracts suppress proactive startup messages.
In ABL, a top-level `ON_START RESPOND` can act as the fallback when branches do not match. If there is no `ON_START`, the first flow step can be the greeting.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Fallback_Greeting_Agent
GOAL: "Recover when startup conditions do not match"
ON_START:
RESPOND: "Welcome. I can help with service, billing, or returns."
BRANCHES:
- IF: session.interaction.current.language == "fr"
RESPOND: "Bonjour. Je peux vous aider avec le service, la facturation ou les retours."
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "How can I help?"
THEN: COMPLETE
```
## How it works
The runtime evaluates the branch. If the language is French, the branch response is selected. If it is not French, the top-level response is used. Because the top-level response is a fallback prelude, the flow can continue into `ready`.
## Common variations
### Start without ON\_START
Use the first flow step as the greeting when the channel or product experience should not send proactive content.
```yaml theme={null}
AGENT: No_Startup_Handler_Agent
GOAL: "Start safely even without a proactive startup response"
FLOW:
entry_point: first_prompt
steps:
- first_prompt
first_prompt:
REASONING: false
RESPOND: "Hello. What can I help you with?"
THEN: COMPLETE
```
## Verification
Test a matching branch, a non-matching branch, and a session without `ON_START`. Confirm every path produces a clear first customer-facing response. In traces, branch misses should not cause an error when a top-level fallback exists.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Omitting the generic fallback | Branch examples focus on the happy path. | Add top-level `ON_START RESPOND` or make the first flow step a safe greeting. |
| Personalizing fallback copy with unverified data | Startup metadata may be missing or untrusted. | Keep fallback copy generic until identity and context are known. |
| Treating a branch miss as an error | Branches are conditional by design. | Test branch misses and verify the fallback response. |
## Troubleshooting
If no greeting appears, confirm whether the channel suppresses startup payloads. If a malformed branch condition is present, the runtime falls back instead of selecting the broken branch; inspect traces for branch diagnostics.
## Production readiness checklist
* Always define a generic startup or first-flow fallback.
* Keep fallback copy broad enough for unknown users.
* Avoid sensitive personalization in fallback messages.
* Test branch miss and missing-context cases.
* Confirm the no-`ON_START` path still has a useful first flow step.
# How to handle messages that contain multiple user intents
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/handle-multiple-intents
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this pattern when customers naturally bundle several tasks into a single message, such as "check my balance, pay my bill, and update my address." The `MULTI_INTENT:` section lets the agent detect, triage, and dispatch those intents instead of silently dropping secondary requests.
## Concept
When a user message contains more than one intent, ABL can detect the individual intents, assess their relationships, and handle them according to a configurable strategy. The `MULTI_INTENT:` section is a top-level ABL block available on both `AGENT:` and `SUPERVISOR:` declarations.
The system uses three layers to decide multi-intent behavior:
1. **Agent-level** — the `MULTI_INTENT:` section in the agent's ABL.
2. **Project-level** — the project runtime configuration (set in Studio or via API).
3. **Platform defaults** — built-in fallback values.
Agent-level settings take precedence over project-level, which takes precedence over platform defaults.
When multi-intent detection is enabled, the NLU pipeline assesses the relationship between detected intents as one of:
* **independent** — intents can be handled in any order with no dependency between them.
* **dependent** — later intents require results from earlier ones.
* **ambiguous** — the system cannot determine the relationship.
This assessment drives strategy resolution, especially for the `auto` strategy.
## Minimal working example
```yaml theme={null}
SUPERVISOR: Multi_Intent_Service_Supervisor
GOAL: "Handle messages that contain more than one customer intent"
MULTI_INTENT:
enabled: true
strategy: primary_queue
max_intents: 3
confidence_threshold: 0.6
queue_max_age_ms: 600000
INTENTS:
check_balance: "Customer wants account balance."
pay_bill: "Customer wants to pay a bill."
update_address: "Customer wants to change address."
HANDOFF:
- TO: Balance_Agent
WHEN: intent.category == "check_balance"
RETURN: true
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer wants to check balance."
- TO: Bill_Payment_Agent
WHEN: intent.category == "pay_bill"
RETURN: true
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer wants to pay a bill."
- TO: Address_Update_Agent
WHEN: intent.category == "update_address"
RETURN: true
CONTEXT:
pass: [customer_id]
summary: "Customer wants to update address."
AGENT: Balance_Agent
GOAL: "Handle balance requests"
AGENT: Bill_Payment_Agent
GOAL: "Handle bill payment"
AGENT: Address_Update_Agent
GOAL: "Handle address changes"
```
The `customer_id` and `account_id` variables passed via `CONTEXT: pass:` must be gathered, set from runtime context, or populated by a tool result before the handoff executes. These are project-local assumptions — replace them with the actual variable names your agents produce.
When `history:` is omitted on a `HANDOFF`, the child agent receives the full parent conversation history (the current platform default is `full`).
## How it works
1. The agent receives the user message and multi-intent detection runs as part of the NLU pipeline.
2. Detected intents are filtered by `confidence_threshold` — only intents above the threshold are considered.
3. The number of considered intents is capped at `max_intents`.
4. The NLU layer assesses the relationship between the intents (independent, dependent, or ambiguous).
5. The `strategy` (or `auto` resolution based on the relationship assessment) determines how intents are dispatched.
6. For `primary_queue` and `sequential`, queued intents are surfaced after the current intent completes. Queued intents expire after `queue_max_age_ms` milliseconds.
## MULTI\_INTENT properties
| Property | Type | Default | Valid range | Description |
| - | - | - | - | - |
| `enabled` | boolean | `true` | true/false | Whether multi-intent detection is active. |
| `strategy` | string | `primary_queue` | `primary_queue`, `sequential`, `parallel`, `disambiguate`, `auto` | How detected intents are dispatched. |
| `unknown_relationship_strategy` | string | `parallel` | `sequential`, `parallel`, `disambiguate` | Fallback strategy when the intent relationship cannot be determined. Used by the `auto` strategy and supervisor routing when relationship data is missing. |
| `max_intents` | integer | `3` | 1 - 10 | Maximum number of intents to consider per message. |
| `confidence_threshold` | number | `0.6` | 0.0 - 1.0 | Minimum confidence for a secondary intent to be accepted. |
| `queue_max_age_ms` | integer | `600000` | 0 - 3,600,000 | Time-to-live for queued intents in milliseconds. Expired intents are dropped. |
## Strategy reference
### primary\_queue (default)
Handle the highest-confidence intent immediately and queue the rest. After the primary intent completes, the agent surfaces queued intents one by one for confirmation. The user can accept, decline, or let them expire.
### sequential
Execute detected intents one at a time in authored or detected order. Context from earlier intents carries forward. Use when later intents depend on earlier results — for example, authenticate first, then process payment, then send confirmation.
```yaml theme={null}
AGENT: Sequential_Support
GOAL: "Handle dependent support requests in order"
MULTI_INTENT:
strategy: sequential
max_intents: 4
confidence_threshold: 0.7
FLOW:
entry_point: detect
steps:
- detect
- authenticate
- payment
- confirmation
detect:
REASONING: false
ON_INPUT:
- IF: input contains "pay"
THEN: authenticate
- ELSE:
RESPOND: "How can I help?"
THEN: COMPLETE
authenticate:
REASONING: false
RESPOND: "Authentication complete."
THEN: payment
payment:
REASONING: false
RESPOND: "Payment processed."
THEN: confirmation
confirmation:
REASONING: false
RESPOND: "Confirmation sent."
THEN: COMPLETE
```
### parallel
Fan out to sub-agents simultaneously. Only available for supervisor agents that route to independent child agents. The runtime enforces safety:
* If the agent is not a supervisor, `parallel` downgrades to `sequential`.
* If the relationship between intents is `dependent`, `parallel` downgrades to `sequential`.
* If the relationship is `ambiguous`, `parallel` downgrades to `disambiguate`.
```yaml theme={null}
SUPERVISOR: Parallel_Service_Supervisor
GOAL: "Handle independent customer requests in parallel"
MULTI_INTENT:
enabled: true
strategy: parallel
unknown_relationship_strategy: parallel
HANDOFF:
- TO: Billing_Specialist
WHEN: intent.category == "billing"
RETURN: true
CONTEXT:
pass: [customer_id]
- TO: Shipping_Specialist
WHEN: intent.category == "shipping"
RETURN: true
CONTEXT:
pass: [customer_id, order_id]
AGENT: Billing_Specialist
GOAL: "Handle billing inquiries"
AGENT: Shipping_Specialist
GOAL: "Handle shipping inquiries"
```
### disambiguate
Present detected intents to the user and ask which to handle first. The user selects by number or by exact intent name. The disambiguation prompt text is customizable.
### auto
Let the model decide the strategy at runtime based on the assessed relationship between intents:
* **independent** intents on a supervisor resolve to `parallel`; on a non-supervisor, they resolve to `sequential`.
* **dependent** intents resolve to `sequential` regardless of agent type.
* **ambiguous** intents resolve to `disambiguate`.
```yaml theme={null}
SUPERVISOR: Auto_Router
GOAL: "Route customer requests using relationship-aware strategy"
MULTI_INTENT:
strategy: auto
unknown_relationship_strategy: sequential
max_intents: 5
INTENTS:
billing: "Billing and payment questions."
technical: "Technical support and troubleshooting."
account: "Account management tasks."
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
RETURN: true
- TO: Technical_Agent
WHEN: intent.category == "technical"
RETURN: true
- TO: Account_Agent
WHEN: intent.category == "account"
RETURN: true
AGENT: Billing_Agent
GOAL: "Handle billing questions"
AGENT: Technical_Agent
GOAL: "Handle technical support"
AGENT: Account_Agent
GOAL: "Handle account management"
```
## Customizing user-facing prompts
The disambiguation and queue-surfacing prompts can be customized through the `MESSAGES:` block:
```yaml theme={null}
MESSAGES:
multi_intent_disambiguate_header: "I noticed your message may contain multiple requests. Which would you like me to help with first?"
multi_intent_disambiguate_option: "{{index}}. {{intent}} (confidence: {{confidence}})"
multi_intent_disambiguation_reprompt: "Please choose an option, pause, or cancel."
multi_intent_queued_notice: "I will address your other requests after completing the current one."
multi_intent_queued_follow_up: "Next: {{next_intent}}. Would you like me to help with that?"
```
## Verification
* Parse and compile the ABL to confirm there are no errors or warnings.
* Send a test utterance that contains two or more intents covered by your `INTENTS:` or `HANDOFF:` conditions.
* Inspect traces for `multi_intent_plan_built` to confirm the detected strategy and targets.
* For `primary_queue`, check for `multi_intent_queue_surfaced` traces when the primary intent completes.
* For `disambiguate`, check for `multi_intent_disambiguate_choice` traces.
* For `parallel`, check for `multi_intent_parallel_executed` traces.
* Confirm that a single-intent utterance bypasses multi-intent handling and routes normally.
* Test a message where all intents fall below `confidence_threshold` to verify fallback behavior.
## Production readiness checklist
* Every specialist route has a clear owner and a concise context summary.
* Deterministic conditions use declared, gathered, tool-result, runtime, or returned fields.
* Semantic conditions are quoted as natural-language `WHEN` text.
* Temporary child agents (with `RETURN: true`) produce every field mapped in `ON_RETURN`.
* Fallback behavior is explicit and does not hide missing intent coverage.
* `max_intents` is set to a value that matches realistic customer behavior (typically 2-5); values above 5 may degrade conversation quality.
* `confidence_threshold` is tuned to avoid false positives from low-confidence secondary intents.
* `queue_max_age_ms` is set to a reasonable session window (default 600,000 ms = 10 minutes; maximum 3,600,000 ms = 1 hour).
* For `parallel` strategy on a supervisor, confirm all target agents can operate independently.
* Consider setting `unknown_relationship_strategy` to `sequential` or `disambiguate` if safety is more important than speed.
* If using `auto` strategy, test with messages that produce each relationship type (independent, dependent, ambiguous).
* Review and customize disambiguation and queue prompts via `MESSAGES:` for your brand voice.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Enabling `parallel` on a non-supervisor agent | Only supervisors can fan out to sub-agents | The runtime automatically downgrades to `sequential`, but configure it explicitly to avoid confusion |
| Setting `max_intents` above 5 | Hoping to catch every possible intent | High values fragment the conversation; keep it at 3-5 for production |
| Assuming queued intents never expire | Not realizing `queue_max_age_ms` has a finite default | Set `queue_max_age_ms` to match your expected session duration |
| Using `EXPECT_RETURN` instead of `RETURN` | Copying from older examples | `EXPECT_RETURN` is a legacy alias; use `RETURN: true` or `RETURN: false` |
| Omitting `unknown_relationship_strategy` | Not realizing the field exists | The default is `parallel`; set it to `sequential` or `disambiguate` if your intents might have dependencies |
| Using `parallel` for dependent work | Assuming all fan-out is safe | With `auto` strategy the runtime detects this; with explicit `parallel` it downgrades dependent relationships to `sequential` |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Secondary intents are silently dropped | `confidence_threshold` is too high or `max_intents` is too low | Lower `confidence_threshold` or increase `max_intents`; inspect `multi_intent_plan_built` traces |
| Parallel routes execute sequentially | Agent is not a supervisor, or intents have dependent/ambiguous relationships | Check traces for strategy downgrade; confirm the agent uses `SUPERVISOR:` declaration |
| Disambiguation prompt does not appear | Strategy is not `disambiguate` (or `auto` with ambiguous relationships) | Check `multi_intent_disambiguate_choice` traces; verify strategy config |
| Queued intents expire before the user is asked | `queue_max_age_ms` is too short for the conversation flow | Increase `queue_max_age_ms` (max 3,600,000 ms) |
| User sees default English prompts | Custom `MESSAGES:` block not set | Add customized `multi_intent_disambiguate_header` and `multi_intent_queued_notice` to MESSAGES |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route conversations based on user intent](/agent-platform/abl/how-to/route-by-user-intent).
* [How to ask a clarification question before routing](/agent-platform/abl/how-to/clarify-before-routing).
# How to initialize session variables before the first user turn
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/initialize-session-before-first-turn
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when flow steps, routing, templates, or tools need known session values before the user says anything.
## Concept
Startup initialization belongs in `ON_START`. Values set there are written into session data before the welcome response and before the entry flow runs. This is useful for counters, channel labels, selected locale, feature flags, campaign markers, and any value that should be stable for the rest of the conversation.
Keep initialization deterministic. `ON_START SET` is for values already available at startup, not values that require asking the user.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Session_Init_Agent
GOAL: "Initialize session values before the first flow step"
ON_START:
SET: retry_count = 0
SET: channel_name = session.channel
SET: preferred_language = session.interaction.current.language
RESPOND: "Session is ready."
FLOW:
entry_point: show_context
steps:
- show_context
show_context:
REASONING: false
RESPOND: "Channel {{channel_name}}, language {{preferred_language}}, retries {{retry_count}}."
THEN: COMPLETE
```
## How it works
`SET` assignments run before `RESPOND`. Assignment values are resolved against runtime session values. The flow can use the initialized variables immediately.
## Common variations
### Initialize project or campaign values
Use constants or channel-supplied values for campaign and entry-state variables. This example uses constants; in a real channel integration, those values are usually copied from trusted session metadata.
```yaml expandable=true theme={null}
AGENT: Metadata_Init_Agent
GOAL: "Copy startup metadata into session variables"
ON_START:
SET: campaign_id = "summer-renewal"
SET: landing_page = "member portal"
RESPOND: "Welcome from {{landing_page}}."
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "Campaign {{campaign_id}} is active for this session."
THEN: COMPLETE
```
## Verification
Start a session and inspect session values after initialization. Confirm `retry_count`, `channel_name`, and `preferred_language` are present before the first flow response. In traces, check `dsl_set` events with `source: on_start`.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Setting values that are not available yet | Startup runs before the first user turn. | Use `ON_START SET` only for constants and pre-existing runtime context. |
| Using different names later in the flow | Startup values are easy to rename while drafting. | Reuse the exact variable names in templates, `WHEN`, `PASS`, and tools. |
| Collecting user input with startup `SET` | Initialization and data collection get mixed together. | Use `GATHER` for user-provided values. |
## Troubleshooting
If a variable renders as empty, verify the runtime path exists. For channel-specific fields, confirm the channel actually supplies the value before the session starts. If the value should come from the user, collect it with `GATHER` instead of startup `SET`.
## Production readiness checklist
* Initialize only values available before the first user turn.
* Keep names consistent with later `WHEN`, `PASS`, and template references.
* Avoid writing sensitive values unless they are needed.
* Trace every startup assignment during validation.
* Reset counters intentionally when a new session starts.
# Artemis how-to guides
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/overview
These task-based guides help you build ABL agents. Each guide starts with the decision or pattern you need, then gives you working ABL to copy into a project.
Use the guides alongside the [ABL reference](/agent-platform/abl/reference/language-overview). The reference explains what each section and keyword does. The guides show which sections to combine for a scenario, and why.
## Prerequisites
Before you use these guides:
* Read the [ABL overview](/agent-platform/abl/reference/language-overview) to understand sections, indentation, and the goal-driven execution model.
* Get access to a project where you can create or edit agents.
* Identify the tools, workflows, and knowledge bases your agent uses. The examples reference these by name, so you must replace the placeholder names with your own.
## Guide categories
Choose an agent pattern, split work across agents, and set execution behavior.
Control greetings, startup logic, session setup, and channel differences.
Route each conversation to the owning agent and debug the routing decision.
***
## Agent architecture and design
Decide how many agents to build, how to split their work, and how to configure their execution.
| Guide | Use it when |
| - | - |
| [How to choose between a reasoning agent and a FLOW-based agent](/agent-platform/abl/how-to/choose-reasoning-agent-or-flow) | You need to decide whether an agent should be goal-driven and flexible, step-driven and predictable, or a mix of both. |
| [How to decide whether to build one agent or multiple specialist agents](/agent-platform/abl/how-to/choose-single-agent-or-specialists) | One customer-facing experience could be one broad agent or a supervisor with specialists. |
| [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent) | One entry point must identify what the user needs, choose a specialist, and pass enough context. |
| [How to define clear responsibilities for each enterprise agent](/agent-platform/abl/how-to/define-agent-responsibilities) | An agent is becoming too broad or too vague for builders and support teams to reason about. |
| [How to combine reasoning steps with deterministic FLOW steps](/agent-platform/abl/how-to/combine-reasoning-and-flow-steps) | A conversation needs both flexible judgment and predictable execution. |
| [How to write effective agent goals, personas, instructions, and limitations](/agent-platform/abl/how-to/write-agent-goals-personas-instructions) | An agent needs a clear job boundary, a consistent voice, and explicit limits. |
| [How to use behavior profiles to change agent behavior by context](/agent-platform/abl/how-to/use-behavior-profiles-by-context) | The same agent keeps its core responsibility but must change style, constraints, or tools by context. |
| [How to configure model, timeout, and execution settings for an agent](/agent-platform/abl/how-to/configure-agent-execution-settings) | An agent needs explicit model choice, response limits, timeout boundaries, or history windowing. |
| [How to design reusable agents and modules for large projects](/agent-platform/abl/how-to/design-reusable-agent-modules) | Many agents, tools, and workflows must compose without duplicated responsibilities or routing ambiguity. |
| [How to use EXECUTION.pipeline for agent routing, classification, and short-circuiting](/agent-platform/abl/how-to/use-agent-execution-pipeline) | A supervisor needs classifier-assisted routing before the normal reasoning path decides what to do. |
| [How to configure EXECUTION.pipeline models, modes, thresholds, and fallbacks](/agent-platform/abl/how-to/configure-agent-execution-pipeline) | The execution pipeline exists but needs tuning for model, mode, confidence thresholds, or fallback behavior. |
## Conversation entry and welcome experience
Control what happens before and during the first user turn: greetings, startup logic, session setup, and channel differences.
| Guide | Use it when |
| - | - |
| [How to create a welcome message for a new conversation](/agent-platform/abl/how-to/create-welcome-message) | A conversation should open with a clear first message before the user asks for help. |
| [How to use ON\_START to greet users before the first message](/agent-platform/abl/how-to/use-on-start-welcome) | The agent must run startup logic before the user sends the first turn. |
| [How to personalize a welcome message for returning users](/agent-platform/abl/how-to/personalize-returning-user-welcome) | The agent should recognize a known user and make the first turn specific to that user. |
| [How to run an authentication or profile lookup during welcome](/agent-platform/abl/how-to/run-auth-profile-lookup-on-start) | The first turn should use authenticated context, such as account status or display name. |
| [How to route users from the welcome experience to the right agent](/agent-platform/abl/how-to/route-users-from-welcome) | The first assistant turn should guide the user into the right specialist path. |
| [How to initialize session variables before the first user turn](/agent-platform/abl/how-to/initialize-session-before-first-turn) | Flow steps, routing, templates, or tools need known session values before the user says anything. |
| [How to design channel-specific welcome experiences](/agent-platform/abl/how-to/design-channel-specific-welcome) | Web chat, voice, messaging, or async channels need different startup behavior. |
| [How to handle empty-state and fallback greetings](/agent-platform/abl/how-to/handle-fallback-greetings) | Startup context may be missing, a branch may not match, or a channel may not deliver proactive content. |
## Routing and multi-agent orchestration
Send each conversation to the agent that owns the work, handle unclear or bundled requests, and debug the routing decision.
| Guide | Use it when |
| - | - |
| [How to route users by detected intent](/agent-platform/abl/how-to/route-by-user-intent) | A supervisor classifies what the user is trying to do, then hands the conversation to the specialist that owns it. |
| [How to design supervisor routing with priority gates](/agent-platform/abl/how-to/route-with-supervisor-priority) | Mandatory checks such as authentication, fraud screening, or consent must run before a business route. |
| [How to route by role, channel, language, and session state](/agent-platform/abl/how-to/route-by-role-channel-language-state) | The same user intent must branch by channel, language, role, or current session state. |
| [How to route by tool result, profile lookup, or remembered state](/agent-platform/abl/how-to/route-by-tool-result-or-memory) | Routing depends on profile, case status, risk, entitlement, or a remembered preference, not just the utterance. |
| [How to route unclear or unsupported requests to a fallback agent](/agent-platform/abl/how-to/route-to-fallback-agent) | The supervisor cannot confidently choose a specialist, or the request is outside the supported intent catalog. |
| [How to handle messages that contain multiple user intents](/agent-platform/abl/how-to/handle-multiple-intents) | Customers bundle several tasks into a single message and secondary requests must not be dropped. |
| [How to clarify before routing to a specialist agent](/agent-platform/abl/how-to/clarify-before-routing) | A request is short, ambiguous, overloaded, or missing the one field that determines ownership. |
| [How to switch the active agent during a conversation](/agent-platform/abl/how-to/switch-active-agent) | A supervisor should stop answering directly and let a specialist own the next turn. |
| [How to debug routing decisions in traces](/agent-platform/abl/how-to/debug-routing-traces) | You need to explain why a user reached one specialist, or why a route fell through to fallback. |
# How to personalize a welcome message for returning users
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/personalize-returning-user-welcome
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when the agent should recognize a known user and make the first turn feel specific to that user.
## Concept
Personalization at startup has two moving pieces: a source of user context and an `ON_START` response that uses it. The context can come from runtime session values, channel metadata copied into session values, memory recall, or a startup tool call.
Use only low-risk personalization in the welcome. Do not expose sensitive account details until identity and authorization are established.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Returning_User_Welcome_Agent
GOAL: "Personalize the welcome message for a known user"
TOOLS:
lookup_profile(memberId: string) -> {content: string, first_name: string, tier: string}
description: "Lookup the returning user's profile"
side_effects: false
confirm: never
params:
memberId:
description: "Member identifier supplied by the channel session"
ON_START:
CALL: lookup_profile
WITH:
memberId: session.member_id
AS: profile
RESPOND: "Welcome back {{profile.first_name}}. You are on the {{profile.tier}} plan."
FLOW:
entry_point: ask_need
steps:
- ask_need
ask_need:
REASONING: false
RESPOND: "What can I help you with today?"
THEN: COMPLETE
```
## How it works
The tool call runs during startup. Its result is stored under `profile` because the call uses `AS: profile`. The response interpolates `profile.first_name` and `profile.tier`.
The tool is declared as read-only with `side_effects: false` and `confirm: never`, because a profile lookup should not require the user to approve a side effect.
## Common variations
### Personalize by segment
Use startup `SET` when the value is already known at session start.
```yaml expandable=true theme={null}
AGENT: Segment_Welcome_Agent
GOAL: "Use returning-user segment metadata in the welcome"
ON_START:
SET: customer_segment = "premium"
BRANCHES:
- IF: customer_segment == "premium"
RESPOND: "Welcome back. Your priority support lane is ready."
- ELSE:
RESPOND: "Welcome back. How can I help?"
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "Ready."
THEN: COMPLETE
```
## Verification
Start a session with a known `session.member_id`. Confirm that the startup tool receives the member id, the `profile` value is available in session data, and the welcome uses the returned profile fields. In traces, look for startup tool execution followed by `dsl_respond`.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Exposing sensitive account facts in the welcome | Personalization is mistaken for authorization. | Use only low-risk fields until identity and permissions are verified. |
| Forgetting `AS` on the startup lookup | The response cannot reference a stable result object. | Bind the startup tool result with `AS` and interpolate from that object. |
| Making a read-only lookup look side-effecting | Tool metadata is incomplete. | Declare startup lookups with `side_effects: false`, `confirm: never`, and parameter descriptions. |
## Troubleshooting
If the welcome shows an empty value, verify the tool result shape and the `AS` name. If the tool fails, the runtime logs the startup call error and continues; use a fallback response that does not depend on the tool result when the lookup is not guaranteed.
## Production readiness checklist
* Personalize with non-sensitive fields only.
* Make startup lookup tools read-only.
* Define parameter descriptions for tool inputs.
* Provide a generic fallback when profile data is unavailable.
* Do not assume channel metadata exists unless the channel integration supplies it.
# How to route by role, channel, language, and session state
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/route-by-role-channel-language-state
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Enterprise routing often depends on more than intent. A voice caller may need a different path than a web user, a Spanish-speaking user may need a language-specific specialist, and an administrator may need a privileged-support queue. ABL lets a supervisor combine deterministic context paths and semantic conditions in the same `HANDOFF` list to route on channel, locale, role, entitlement, failure count, or session state.
Use this when the same user intent must branch by channel, language, role, or current session state.
## Concept
A supervisor evaluates `HANDOFF` entries in authored order. Each entry has a `WHEN` condition that can be either:
* **Deterministic**: a structured expression referencing session data values, such as `session.channel == "voice"` or `session.interaction.current.language == "es"`. The runtime evaluates these against session state and produces a boolean result without involving the model.
* **Semantic**: a natural-language description of when the route should activate, such as `"the user is an administrator and the account is locked"`. The runtime defers these to the model, which decides based on conversation context.
You can mix both in the same HANDOFF list. Deterministic conditions are reliable for known, platform-provided values. Semantic conditions are flexible for signals that come from conversation context, identity systems, or policy rules that are not yet in structured session data.
When no HANDOFF condition matches, the supervisor retains ownership and may respond directly or re-prompt the user. Always include a catch-all route or ensure the supervisor has a meaningful direct-response capability.
### Key context paths
| Path | Source | Type | Example |
| - | - | - | - |
| `session.channel` | Set at session initialization from the channel type (for example, `web`, `voice`, `web_chat`, `api`) | deterministic | `session.channel == "voice"` |
| `session.interaction.current.language` | Resolved from the interaction context (message, session preference, channel, project, or agent default) | deterministic | `session.interaction.current.language == "es"` |
| `session.interaction.current.locale` | Resolved alongside language | deterministic | `session.interaction.current.locale == "es-MX"` |
| `session.interaction.current.timezone` | Resolved alongside language | deterministic | `session.interaction.current.timezone == "America/Mexico_City"` |
| User role, account state, entitlements | Not platform-provided; must be populated via ON\_START SET, GATHER, tool output, caller context, or memory | semantic or deterministic (if populated) | `"the user is an administrator"` or `user_role == "admin"` if gathered |
### History default
When a HANDOFF entry omits the `history` property, the child agent receives the **full** parent conversation history. This is the platform default. To limit what the child sees, set `history` explicitly:
* `full` (default when omitted): complete parent conversation history
* `auto`: uses the handoff summary when available, otherwise the last 5 messages
* `summary_only`: only the summary text, no raw messages
* `none`: no history
* `{ mode: last_n, count: }`: the last N messages
## Minimal working example
This supervisor routes by channel, language, role, and a general fallback. Each variable in `pass` must be populated before routing — typically via `ON_START SET`, `GATHER`, a tool call, or caller context from the SDK/channel.
```yaml theme={null}
SUPERVISOR: Context_Aware_Routing_Supervisor
GOAL: "Route by user role, channel, language, and session state"
HANDOFF:
- TO: Voice_Escalation_Agent
WHEN: session.channel == "voice"
EXPECT_RETURN: false
PASS: [customer_id, conversation_summary]
SUMMARY: "Voice caller routed to voice-specific handling."
HISTORY: auto
- TO: Spanish_Service_Agent
WHEN: session.interaction.current.language == "es"
EXPECT_RETURN: false
PASS: [customer_id, conversation_summary]
SUMMARY: "Customer should be served in Spanish."
- TO: Admin_Support_Agent
WHEN: "the user is an administrator and the account is locked or requires privileged support"
EXPECT_RETURN: false
PASS: [customer_id, account_id]
SUMMARY: "Admin user needs account lock support."
- TO: Standard_Service_Agent
WHEN: "the user needs ordinary support and no channel, language, role, or state override applies"
EXPECT_RETURN: false
PASS: [customer_id, conversation_summary]
SUMMARY: "Customer needs standard service."
AGENT: Voice_Escalation_Agent
GOAL: "Handle voice escalation"
AGENT: Spanish_Service_Agent
GOAL: "Handle Spanish-language service"
AGENT: Admin_Support_Agent
GOAL: "Handle administrator support"
AGENT: Standard_Service_Agent
GOAL: "Handle standard support"
```
**Variable sourcing assumptions:** `customer_id` and `account_id` are populated from caller context or an ON\_START lookup. `conversation_summary` is a platform-provided session variable. These must exist in session data before the HANDOFF evaluates.
This example uses the flat CONTEXT shorthand (`PASS:`, `SUMMARY:`, `HISTORY:` as direct siblings of the HANDOFF entry). The nested `CONTEXT:` wrapper form is also supported:
```yaml theme={null}
- TO: Voice_Escalation_Agent
WHEN: session.channel == "voice"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, conversation_summary]
summary: "Voice caller routed to voice-specific handling."
history: auto
```
Both forms produce identical results.
## How it works
1. The supervisor receives the user message and evaluates HANDOFF entries in authored order.
2. For each entry, the runtime classifies the WHEN condition:
* **Structured conditions** (containing operators like `==`, `!=`, `AND`, `OR`, `IS SET`, comparisons) are evaluated deterministically against session data values.
* **Natural-language conditions** (free-form text without operators) are deferred to the model, which decides based on conversation context and the condition text.
3. The first matching handoff transfers control to the target agent. The target receives the declared `pass` variables, the `summary`, and conversation history according to the `history` strategy (default: `full`).
4. When `EXPECT_RETURN: false`, the child becomes the new active agent permanently. When `EXPECT_RETURN: true`, the child returns control to the supervisor via `ON_RETURN`.
## Common variations
### Combining deterministic and semantic conditions
You can combine a deterministic check with a semantic qualifier in a single WHEN using AND:
```yaml theme={null}
- TO: VIP_Voice_Agent
WHEN: session.channel == "voice" AND "the user is a VIP customer"
EXPECT_RETURN: false
PASS: [customer_id, vip_tier]
SUMMARY: "VIP voice caller needs premium support."
```
The runtime evaluates the deterministic part first. If it passes, the semantic part is deferred to the model. Both must be true for the route to match.
### Routing on locale instead of language
```yaml theme={null}
- TO: LatAm_Agent
WHEN: session.interaction.current.locale == "es-MX"
EXPECT_RETURN: false
PASS: [customer_id]
SUMMARY: "Mexican Spanish customer."
```
### Routing with explicit priority
Use `PRIORITY` to override authored-order evaluation. Lower values are evaluated first:
```yaml theme={null}
HANDOFF:
- TO: Emergency_Agent
WHEN: "the user reports an emergency or safety concern"
PRIORITY: 1
EXPECT_RETURN: false
PASS: [customer_id]
SUMMARY: "Emergency escalation."
- TO: Standard_Agent
WHEN: "the user needs general support"
PRIORITY: 10
EXPECT_RETURN: false
PASS: [customer_id]
SUMMARY: "Standard support."
```
### Return-expected handoff with ON\_RETURN
When a specialist should return control after completing its task:
```yaml theme={null}
- TO: Language_Detection_Agent
WHEN: "the user's language is unclear from the initial message"
EXPECT_RETURN: true
PASS: [customer_id]
SUMMARY: "Detect preferred language."
ON_RETURN:
MAP:
detected_language: language_result
```
### Adding ON\_FAILURE for robustness
```yaml theme={null}
- TO: Voice_Escalation_Agent
WHEN: session.channel == "voice"
EXPECT_RETURN: false
PASS: [customer_id]
SUMMARY: "Voice caller."
ON_FAILURE: "I'm unable to connect you to the voice team right now. Let me help you directly."
```
`ON_FAILURE` fires when the target agent cannot be found or the dispatch fails before the target accepts the handoff.
## Verification
* Validate the ABL document parses and compiles without errors or warnings.
* Send a test message from a voice channel and confirm the `Voice_Escalation_Agent` is selected. Inspect the trace for a `routing_primitive_when` event showing `kind: deterministic_pass` for the `session.channel == "voice"` condition.
* Send a test message with the interaction context language set to `es` and confirm the `Spanish_Service_Agent` is selected.
* Send a test message as an administrator (with role context populated) and confirm the `Admin_Support_Agent` is selected via model-deferred semantic evaluation.
* Send a test message that matches no specific override and confirm the `Standard_Service_Agent` is selected.
* Confirm that each target agent receives the expected `pass` variables and conversation history.
## Production readiness checklist
* Every variable in `pass` lists has a verified source: gathered field, ON\_START SET, caller context, tool output, or memory. Variables that are not populated before routing will be passed as empty/null.
* Deterministic conditions use platform-provided paths (`session.channel`, `session.interaction.current.language`) or variables with known, reliable sources.
* Semantic conditions describe clear, unambiguous selection criteria. Vague conditions may cause inconsistent routing.
* The `history` default is `full` when omitted. If children should not see full history, set `history` explicitly on each handoff.
* A catch-all route or direct-response capability exists for messages that match no specific route.
* Each specialist agent is defined and reachable (either inline or as an imported agent reference).
* `ON_FAILURE` is set on critical routes where target unavailability must produce a user-facing message rather than silence.
* Route distinctness: each deterministic WHEN condition is mutually exclusive or ordered so the first match wins correctly. Overlapping semantic conditions may produce non-deterministic routing.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using `session.interaction.current.channel` for channel routing | Confusion between interaction context (language/locale/timezone) and session-level channel | Use `session.channel` for channel routing |
| Assuming user role is in session data | Role is not a platform-provided field | Populate role via ON\_START lookup, GATHER, tool call, or caller context before routing |
| Omitting `history:` without realizing children get full history | The default changed from `auto` to `full` | Set `history` explicitly when children should not see the full conversation |
| Mixing unquoted NL text with expression operators | The condition classifier may treat it as a malformed structured expression | Quote NL routing text or ensure it has no `==`, `!=`, `AND`, `OR`, `IS SET` operators |
| Creating separate specialists for every channel when behavior is identical | Over-specialization adds maintenance cost | Only create channel-specific agents when the agent behavior genuinely differs |
| Not populating PASS variables before routing | Variables passed as null/empty if not sourced | Ensure each variable is set via ON\_START, GATHER, tool, caller context, or memory before the HANDOFF evaluates |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Channel condition never matches | Wrong context path or channel type string | Confirm `session.channel` value in traces; common values are `web`, `voice`, `web_chat`, `api`, `digital` |
| Language condition never matches | Interaction context not populated | Check `session.interaction.current.language` in traces; verify the channel or SDK sends language context |
| Semantic WHEN routes inconsistently | Model interprets NL condition differently across turns | Make the condition text more specific; consider adding a deterministic guard variable instead |
| No route matches and the supervisor goes silent | No catch-all and `direct_response_allowed` is false | Add a fallback HANDOFF with a broad semantic WHEN, or set the supervisor role to `orchestrator` |
| Child agent does not have expected context | Variables not populated before handoff | Verify each variable in `pass` is set before routing; check ON\_START or GATHER ordering |
| Child agent receives too much history | Default `history` is `full` | Set `history: summary_only` or `history: none` on the handoff entry |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route conversations based on user intent](/agent-platform/abl/how-to/route-by-user-intent).
* [How to debug routing decisions in traces](/agent-platform/abl/how-to/debug-routing-traces).
# How to route by tool result, profile lookup, or remembered state
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/route-by-tool-result-or-memory
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this when routing depends on customer profile, case status, fraud risk, policy coverage, entitlement, remembered preference, or any state loaded before the handoff — not just the user's utterance.
## Concept
Some routes can't be decided from what the user typed. The supervisor first calls a read-only lookup tool, binds the result with `AS`, then routes with deterministic conditions over that result's fields. This is the pattern for premium routing, existing-case routing, risk routing, entitlement routing, and remembered-preference routing.
One behavior is worth knowing before you move past the happy path: **you don't have to copy tool-result fields into separate session variables before routing on them.** You can reference the `AS`-bound object's fields directly in `WHEN`; a flat copy (`SET profile_tier = profile.tier`) is only useful if you also want to pass a shorter, flatter set of fields through `CONTEXT.pass`.
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Tool_Result_Routing_Supervisor
GOAL: "Look up customer state before selecting a specialist"
TOOLS:
lookup_customer_profile(customer_id: string) -> object
description: "Return customer tier, risk, and open-case status for routing."
side_effects: false
confirm: never
params:
customer_id:
description: "Customer identifier to look up."
ON_START:
CALL: lookup_customer_profile
WITH:
customer_id: customer_id
AS: profile
SET:
profile_tier = profile.tier
has_open_case = profile.has_open_case
fraud_risk = profile.fraud_risk
HANDOFF:
- TO: Fraud_Review_Agent
WHEN: profile.fraud_risk == "high"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, profile_tier, fraud_risk]
summary: "Customer profile indicates high fraud risk."
- TO: Existing_Case_Agent
WHEN: profile.has_open_case == true
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, profile_tier, has_open_case]
summary: "Customer already has an open case."
- TO: Premium_Service_Agent
WHEN: profile.tier == "premium"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, profile_tier]
summary: "Premium customer needs support."
- TO: Standard_Service_Agent
WHEN: "no fraud, open-case, or premium routing condition is matched"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id]
summary: "Standard customer support route."
AGENT: Fraud_Review_Agent
GOAL: "Review fraud-risk requests"
AGENT: Existing_Case_Agent
GOAL: "Continue existing cases"
AGENT: Premium_Service_Agent
GOAL: "Serve premium customers"
AGENT: Standard_Service_Agent
GOAL: "Serve standard customers"
```
`customer_id` is referenced as both a tool parameter and a `CONTEXT.pass` field with no declared source in this example — treat it as a project-local assumption (typically populated earlier from authentication or a prior turn) and declare it via `MEMORY` in your actual project.
## How it works
* `ON_START` calls the read-only lookup tool once, before any `HANDOFF` is evaluated, and binds the result to `profile` with `AS`.
* Each `HANDOFF`'s `WHEN` is evaluated in authored order. Every condition here is a plain equality comparison (`profile.fraud_risk == "high"`, `profile.has_open_case == true`, `profile.tier == "premium"`) — if the field hasn't resolved yet, the comparison simply evaluates `false` rather than erroring, so you don't need to write `profile.fraud_risk IS SET AND profile.fraud_risk == "high"` yourself.
* The `SET` step that copies `profile.tier`/`profile.has_open_case`/`profile.fraud_risk` into flat variables is not required for the `WHEN` conditions to work — they reference `profile.*` directly. The copies exist here only so the flatter names can be listed in `CONTEXT.pass`; you can pass `profile` itself (or a subset via a shorthand) instead if you prefer not to duplicate fields.
* The fallback route uses a quoted natural-language condition. Keep this kind of fallback text in lowercase, ordinary sentence form — a quoted condition containing an uppercase `AND`/`OR`/`NOT` can be misclassified by the runtime as a structured deterministic expression rather than natural language, which will not evaluate the way you intend. Lowercase words like "or"/"and" inside ordinary sentences (as in this example) are handled correctly.
* Since none of the `HANDOFF` entries declare `HISTORY`, each child now receives the full conversation history by default (the current platform default when `HISTORY` is omitted). Set `HISTORY: auto` (or another explicit strategy) if you specifically want bounded/summary history instead.
* `lookup_customer_profile` is declared with `side_effects: false` and `confirm: never` because it's a read-only enrichment call used purely to decide a route — it should never require user confirmation and should never be confused with a state-changing action.
## Common variations
### Routing on remembered/persistent state instead of a live lookup
Replace the `ON_START CALL ... AS: profile` step with a read from persistent memory (see the memory HowTos for declaring and recalling scoped memory), then route on the recalled fields the same way — the `WHEN` conditions don't change shape.
### Passing the bound object instead of flat copies
If you don't need a smaller/renamed field set in `CONTEXT.pass`, skip the flat `SET` copies entirely and pass `profile` (or specific `profile.*` paths) directly.
## Verification
* Parse and compile the ABL and confirm there are no parser or compiler errors/warnings.
* Test at least one utterance that resolves to each of the four routes, including the fallback path (no fraud, no open case, non-premium tier).
* Inspect the trace for the selected target and the evaluated condition — you'll see your literal authored condition text, since HANDOFF conditions are not rewritten by the compiler.
* Confirm the fallback route's semantic condition resolves as intended by testing an utterance where all three deterministic conditions are false.
## Production readiness checklist
* The lookup tool is declared read-only (`side_effects: false`) and never requires confirmation for a routing-only call.
* Every specialist route has a clear owner and a concise context summary.
* Deterministic conditions reference fields that are actually present on the tool's declared return shape.
* Quoted natural-language fallback text avoids uppercase `AND`/`OR`/`NOT`.
* `customer_id` (or any other assumed pre-existing variable) has a real declared source in your project, not just an assumption carried over from this example.
* Fallback behavior is explicit and does not hide missing routing coverage.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Calling a side-effecting tool just to decide a route | Reusing an existing action tool instead of declaring a dedicated read-only lookup | Declare a separate tool with `side_effects: false` and `confirm: never` for routing-only enrichment |
| Assuming a flat `SET` copy is required before routing on a tool result | Copying feels like the "safe" pattern | Reference the `AS`-bound object's fields directly in `WHEN`; only copy when you need a flatter `CONTEXT.pass` shape |
| Writing uppercase `AND`/`OR`/`NOT` inside a quoted natural-language fallback `WHEN` | It reads naturally to a human author | Keep quoted fallback conditions in ordinary lowercase sentence form |
| Passing the entire profile object when the child only needs tier, risk, or case state | Convenience | Pass only the specific fields each specialist needs |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| The fallback route fires even when a deterministic condition should have matched | The tool result field wasn't populated by the time `HANDOFF` conditions were evaluated | Confirm `ON_START CALL ... AS` completed before `HANDOFF` evaluation; check the tool's actual return shape |
| A quoted fallback condition behaves unpredictably | Uppercase `AND`/`OR`/`NOT` inside the quoted text was misclassified as a structured expression | Rewrite the fallback text in ordinary lowercase sentence form |
| A child agent has more/less conversation context than expected | `HISTORY` was omitted and the current platform default (`full`) applied | Set an explicit `HISTORY` strategy if you need bounded/summary history |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route conversations based on user intent](/agent-platform/abl/how-to/route-by-user-intent).
# How to route users by detected intent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/route-by-user-intent
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this pattern when a supervisor agent classifies what the user is trying to do, then hands the conversation to the specialist that owns that work.
## Concept
Intent routing uses three ABL surfaces working together:
* **INTENTS** declares the classifier categories. Each category has a description that tells the model what kinds of requests belong to it.
* **HANDOFF** is the executable routing surface. Each entry names a target agent, the condition that triggers the handoff, and the context the child receives.
* **WHEN** conditions on each handoff entry can be deterministic expressions (`intent.category == "billing"`) or semantic natural-language text (`"the user request is unclear"`). The platform detects the form automatically: multi-word quoted text without structured operators routes through model reasoning; expressions with operators like `==` route through deterministic evaluation.
Two decisions shape every intent-routing supervisor:
1. **History strategy** — By default, the child agent receives the full parent conversation history (`history: full`). This is the platform default when `history` is omitted. Choose `summary_only`, `none`, `auto`, or `last_n` when the child should see less. See [Common variations](#common-variations) for the options.
2. **Return behavior** — `EXPECT_RETURN: false` (the default) transfers ownership permanently. `EXPECT_RETURN: true` pauses the parent and resumes it when the child returns. Use `ON_RETURN` to map child results back to parent variables.
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Intent_Routing_Supervisor
GOAL: "Route customer requests to the specialist that owns the detected intent"
MEMORY:
customer_id:
type: string
source: session
account_id:
type: string
source: session
INTENTS:
billing: "Questions about invoices, payment status, unpaid balances, or payment methods."
technical_support: "Problems with product access, errors, setup, or troubleshooting."
cancellation: "Requests to cancel, downgrade, or stop renewal."
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer needs billing help."
- TO: Technical_Support_Agent
WHEN: intent.category == "technical_support"
CONTEXT:
pass: [customer_id]
summary: "Customer needs technical support."
- TO: Retention_Agent
WHEN: intent.category == "cancellation"
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer is asking to cancel or downgrade."
- TO: General_Service_Agent
WHEN: "the user request is unclear or does not match billing, technical support, or cancellation"
CONTEXT:
pass: [customer_id]
summary: "Customer needs general service triage."
AGENT: Billing_Agent
GOAL: "Resolve billing requests"
AGENT: Technical_Support_Agent
GOAL: "Resolve technical support requests"
AGENT: Retention_Agent
GOAL: "Handle cancellation and downgrade conversations"
AGENT: General_Service_Agent
GOAL: "Clarify and route unsupported requests"
```
The `MEMORY` section declares `customer_id` and `account_id` as session variables so the `pass` fields have a defined source. Any variable you pass must come from MEMORY, GATHER, a tool result, or runtime context.
## How it works
1. The supervisor receives the user message and retains ownership until a handoff condition matches.
2. The platform evaluates the INTENTS classifier against the user message, producing `intent.category`.
3. HANDOFF entries are evaluated in authored order (override with `PRIORITY` when needed). The first matching condition wins.
4. The matched child receives: the `pass` variables, the `summary` text, and conversation history controlled by the `history` strategy. When `history` is omitted, the child receives the **full parent conversation** — this is the platform default (`full`).
5. If `EXPECT_RETURN: false` (the default), the child owns the conversation permanently. If `EXPECT_RETURN: true`, the child runs and returns control to the parent, optionally mapping result variables through `ON_RETURN`.
6. If no HANDOFF condition matches, the supervisor continues with its own GOAL. A semantic fallback entry (like the `General_Service_Agent` above) prevents unmatched requests from going unhandled.
## Common variations
### Control conversation history with `history`
When the child should not receive the full parent transcript, set `history` explicitly:
```yaml theme={null}
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer needs billing help."
history: summary_only
```
| Value | Behavior |
| - | - |
| `full` | Platform default. The complete parent conversation history is passed. |
| `auto` | Uses the summary when available; falls back to the last 5 messages. |
| `none` | No conversation history. The child starts fresh with only `pass` variables and `summary`. |
| `summary_only` | Only the `summary` text is passed — no raw messages. |
| `{ mode: last_n, count: N }` | The last N messages from the parent are passed. |
Choose `summary_only` or `none` when the child is a focused specialist that should not be influenced by earlier conversation turns. Choose `full` (or omit) when the child needs the complete conversation record.
### Shorthand context syntax
`pass`, `summary`, and `history` can appear as direct siblings of the handoff entry instead of nested under `CONTEXT:`:
```yaml theme={null}
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
pass: [customer_id, account_id]
summary: "Customer needs billing help."
history: summary_only
```
Both forms compile identically. The nested `CONTEXT:` form is preferred when you also need `set` or `memory_grants`.
### Return to the parent with ON\_RETURN
When a specialist should report back to the supervisor after completing its task:
```yaml expandable=true theme={null}
HANDOFF:
- TO: Verification_Agent
WHEN: intent.category == "verification_needed"
EXPECT_RETURN: true
CONTEXT:
pass: [customer_id]
summary: "Customer needs identity verification."
history: summary_only
ON_RETURN:
action: continue
map:
verified: verification_result
```
`EXPECT_RETURN: true` pauses the parent. When the child returns, `ON_RETURN.map` copies child variables into the parent scope. `action: continue` resumes the parent's flow; `action: resume_intent` re-runs routing with the original user intent and the returned context.
The legacy keyword `RETURN` is accepted as an alias for `EXPECT_RETURN`.
### Set evaluation order with PRIORITY
When multiple handoff conditions could match the same utterance, use `PRIORITY` to control evaluation order. Lower values are evaluated first:
```yaml expandable=true theme={null}
HANDOFF:
- TO: Urgent_Support_Agent
WHEN: intent.category == "technical_support" and priority_level == "urgent"
PRIORITY: 0
CONTEXT:
pass: [customer_id]
summary: "Urgent technical support request."
- TO: Technical_Support_Agent
WHEN: intent.category == "technical_support"
PRIORITY: 1
CONTEXT:
pass: [customer_id]
summary: "Technical support request."
```
Without `PRIORITY`, entries are evaluated in authored order.
### Handle dispatch failures with ON\_FAILURE
`ON_FAILURE` defines what happens when the handoff dispatch itself fails — for example, the target agent is not found or pre-transfer validation fails. It does not handle child-agent runtime errors.
```yaml theme={null}
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer needs billing help."
ON_FAILURE: ESCALATE
```
| Value | Behavior |
| - | - |
| `CONTINUE` | Silently continue in the current agent. |
| `ESCALATE` | Trigger human escalation. |
| `RESPOND "message"` | Send a message and continue in the current agent. |
## Verification
* Send a test utterance for each intent category and confirm the correct child agent is selected. Check the trace for the matched HANDOFF entry and the condition result.
* Send an ambiguous utterance and confirm the fallback entry handles it. Verify the trace shows the semantic WHEN evaluation.
* Inspect the trace for passed context: confirm `pass` variables are present in the child's session and the `summary` appears in the child's context.
* If `EXPECT_RETURN: true` is used, verify the parent resumes and `ON_RETURN.map` variables appear in the parent's session after the child returns.
* Verify the `history` strategy by checking what conversation messages the child receives. With `full` (or omitted), the child should see the complete parent transcript. With `summary_only`, the child should see only the summary text.
## Production readiness checklist
* Every specialist route has a clear owner and a concise context summary.
* Every `pass` variable has a declared source (MEMORY, GATHER, tool result, or runtime context).
* Deterministic conditions use declared fields; semantic conditions are quoted as natural-language `WHEN` text.
* Choose a `history` strategy appropriate for each child. The default `full` passes the entire parent transcript, which may include irrelevant turns and increase token usage.
* Add `ON_FAILURE` to critical handoff entries so dispatch failures have a defined recovery path.
* Fallback behavior is explicit and does not hide missing intent coverage.
* Test at least one utterance per intent category, one ambiguous utterance, and one utterance outside all categories.
* Temporary child agents (`EXPECT_RETURN: true`) produce every field mapped in `ON_RETURN`.
* Verify handoff depth does not exceed the platform limit of 10 nested handoffs.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Authoring a `ROUTING:` section | Confusion between conceptual routing and the ABL surface | The parser rejects `ROUTING:` and points to `HANDOFF:`. Use `HANDOFF:` for all routing entries. |
| Leaving semantic prose unquoted | The WHEN condition looks like an expression with undefined variables | Quote multi-word natural-language conditions so the platform routes them through model reasoning instead of expression evaluation. |
| Assuming the child starts with a blank slate | The default history strategy is `full`, not `none` | Set `history: none` or `history: summary_only` explicitly when you want the child to start fresh. |
| Passing variables without a declared source | The variables compile but have no value at runtime | Declare every `pass` variable in MEMORY, GATHER, or as a tool/runtime output before referencing it. |
| Routing every unmatched request to the nearest specialist | No explicit fallback; the closest label absorbs unrelated requests | Add a dedicated fallback agent with a semantic WHEN condition for unclear or unmatched requests. |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Child agent sees the full conversation even though only `pass` variables were declared | `history` defaults to `full` when omitted | Set `history: summary_only` or `history: none` in the CONTEXT block. |
| Handoff never fires for a valid utterance | WHEN condition uses an undefined variable or has a syntax error | Check that the WHEN expression references a valid field (for example, `intent.category`) and the INTENTS section defines the expected category. |
| Compiler warning about ungrounded pass fields | `pass` variables are not declared in MEMORY, GATHER, or a tool output | Declare each variable source or mark it as a project-local runtime value. |
| Fallback agent handles requests that should match a specific intent | Intent categories overlap or the classifier description is ambiguous | Refine INTENTS descriptions to be mutually exclusive; use `PRIORITY` to control evaluation order for overlapping conditions. |
| Parent does not resume after child completes | `EXPECT_RETURN` is set to `false` or omitted (default) | Set `EXPECT_RETURN: true` on the handoff entry. Confirm the child agent completes and returns control. |
| `ON_RETURN.map` variables are missing in parent scope | Child agent does not produce the expected output variables | Verify the child agent sets the mapped variables before returning. |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
# How to route unclear or unsupported requests to a fallback agent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/route-to-fallback-agent
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
A fallback agent is not a dumping ground; it is a specialist for clarification, unsupported requests, conflicting intents, and safe recovery. The fallback can either complete the conversation or return a clarified signal so the supervisor can resume routing.
Use this when the supervisor cannot confidently choose a specialist or when the user asks for something outside the supported intent catalog.
## Concept
Keep clear deterministic or intent routes first, and put a quoted semantic fallback condition last for vague, unsupported, or conflicting requests. When the fallback should re-enter normal routing after clarifying, use `EXPECT_RETURN: true` with `ON_RETURN: { action: resume_intent, MAP: {...} }` — the fallback child gathers whatever's missing, and `MAP` copies its result back into a parent-owned variable your supervisor can route on next turn.
One thing affects how this looks once you look past the syntax: **`HANDOFF` history defaults to `full` when omitted.** Every route below omits `HISTORY`, so each child (including the fallback) now receives the entire conversation by default.
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Fallback_Routing_Supervisor
GOAL: "Send clear requests to specialists and unclear requests to a fallback agent"
INTENTS:
billing: "Billing and payment requests."
technical_support: "Product troubleshooting requests."
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer needs billing help."
- TO: Technical_Support_Agent
WHEN: intent.category == "technical_support"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, issue_summary]
summary: "Customer needs technical support."
- TO: Fallback_Triage_Agent
WHEN: "the request is vague, unsupported, conflicting, or missing enough information to choose a specialist"
EXPECT_RETURN: true
CONTEXT:
pass: [customer_id, conversation_summary]
summary: "Clarify the customer's request and return if a specialist can be chosen."
ON_RETURN:
action: resume_intent
MAP:
clarified_intent: clarified_intent
AGENT: Billing_Agent
GOAL: "Resolve billing requests"
AGENT: Technical_Support_Agent
GOAL: "Resolve technical support requests"
AGENT: Fallback_Triage_Agent
GOAL: "Clarify unsupported or ambiguous requests"
MEMORY:
SESSION:
- clarified_intent
GATHER:
clarified_intent:
TYPE: string
PROMPT: "Which area should handle this request?"
REQUIRED: true
COMPLETE:
- WHEN: clarified_intent IS SET
RESPOND: "Thanks, I captured the clarified route."
```
`customer_id`, `account_id`, `issue_summary`, and `conversation_summary` are shown as passed context with no declared source in this example — treat them as project-local assumptions (typically populated from authentication or an earlier turn) and declare them via `MEMORY` in your actual project.
## How it works
* The fallback route's `WHEN` is a quoted natural-language condition, evaluated only when neither deterministic route above it matched.
* `EXPECT_RETURN: true` makes `Fallback_Triage_Agent` a temporary child: it gathers `clarified_intent` from the user, then its `COMPLETE` fires once that field `IS SET`.
* `ON_RETURN.action: resume_intent` tells the supervisor to resume routing after the child returns. `MAP: { clarified_intent: clarified_intent }` copies the child's gathered value (left side, child key) into the parent supervisor's own `clarified_intent` session variable (right side, parent key) — declared here in the supervisor's own `MEMORY.SESSION`.
* If you don't need to map any fields back — the fallback child only needs to signal "done, try routing again" — you can use the simpler shorthand `ON_RETURN: "resume_intent"` instead of the structured `action`/`MAP` block.
* Since `HISTORY` is omitted from every `HANDOFF`, each child (including the fallback) receives the full conversation history by default.
## Common variations
* Clarify and return to the supervisor (shown above).
* Complete unsupported requests directly in the fallback agent with a safe explanation, without ever returning to the supervisor (`EXPECT_RETURN: false`, or omit `ON_RETURN` and let the fallback's own `COMPLETE` end the conversation).
* Escalate to a human after repeated clarification failures instead of looping the fallback indefinitely.
* Use the plain string shorthand `ON_RETURN: "resume_intent"` when the fallback child doesn't need to map any fields back.
## Verification
* Parse and compile the ABL and confirm there are no parser or compiler errors/warnings.
* Test at least one matching utterance for each specialist route and one clearly ambiguous utterance that should hit the fallback.
* Confirm the fallback's `COMPLETE` fires once `clarified_intent IS SET`, and that the parent's `clarified_intent` reflects the child's gathered value after return.
* Inspect the trace for the selected target, the evaluated condition (matches your literal authored text for the deterministic routes), and the `ON_RETURN`/`MAP` application on return.
## Production readiness checklist
* Every specialist route has a clear owner and a concise context summary.
* The fallback's `MAP` targets a variable the parent actually declares and routes on next turn.
* Semantic conditions are quoted as natural-language `WHEN` text.
* Fallback behavior is explicit and does not hide missing intent coverage — use trace review to find requests that keep landing in fallback and add real routes for them.
* Any variable assumed to already exist (like `customer_id` above) has a real declared source in your project.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using literal `true` as the only route | Looks like a simple catch-all | Only do this intentionally, and document that it's a deliberate catch-all, not a missing condition |
| Mapping a clarified field the fallback child never produces | `MAP` target doesn't match anything the child's `GATHER`/`COMPLETE` actually sets | Confirm the child key in `MAP` matches a field the child truly returns |
| Letting fallback silently absorb missing intent coverage | Fallback "just works" so gaps go unnoticed | Review fallback-route traces periodically and add real specialist routes for recurring patterns |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| The parent's `clarified_intent` is never updated after the fallback returns | `MAP` key mismatch, or the child never actually sets the mapped field | Confirm child-side field name in `MAP` matches what `GATHER`/`COMPLETE` produces |
| Routing doesn't resume after the fallback returns | `ON_RETURN.action` isn't `resume_intent`, or `EXPECT_RETURN` was `false` | Confirm `EXPECT_RETURN: true` and `ACTION: resume_intent` are both set |
| A specialist route unexpectedly falls through to the fallback | The referenced field wasn't populated yet, or the condition evaluated differently than expected | Inspect the trace's evaluated condition and `evaluatedContext` |
| Child agent has more/less conversation context than expected | `HISTORY` omitted; current default (`full`) applied | Set an explicit `HISTORY` strategy if bounded/summary history is intended |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route conversations based on user intent](/agent-platform/abl/how-to/route-by-user-intent).
# How to route users from the welcome experience to the right agent
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/route-users-from-welcome
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when the first screen or first assistant turn should guide the user into the right specialist path.
## Concept
Welcome routing turns the first interaction into a routing signal. The signal can come from a button, an entry-page value copied into session context, or the user's first typed response. Once the signal is known, normal handoff routing should move the conversation to a specialist agent.
Do not reference a specialist agent unless it exists in the project. A routing example is only operational when the target agents are defined and the value used in `WHEN` is actually collected or set.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Welcome_Router_Agent
GOAL: "Route users from the welcome experience to the right specialist"
GATHER:
startup_choice:
PROMPT: "Which option did you choose?"
TYPE: string
REQUIRED: true
HANDOFF:
- TO: Billing_Agent
WHEN: startup_choice == "billing"
PASS: [account_id, startup_choice]
SUMMARY: "User selected billing from the welcome experience"
- TO: Returns_Agent
WHEN: startup_choice == "returns"
PASS: [account_id, startup_choice]
SUMMARY: "User selected returns from the welcome experience"
ON_START:
RESPOND: "Welcome. Choose billing or returns."
ACTIONS:
- BUTTON: "Billing" -> billing
VALUE: "billing"
- BUTTON: "Returns" -> returns
VALUE: "returns"
FLOW:
entry_point: capture_choice
steps:
- capture_choice
capture_choice:
REASONING: false
GATHER:
startup_choice:
PROMPT: "Which option did you choose?"
TYPE: string
REQUIRED: true
THEN: COMPLETE
```
```yaml theme={null}
AGENT: Billing_Agent
GOAL: "Handle billing questions"
FLOW:
entry_point: start
steps:
- start
start:
REASONING: false
RESPOND: "I can help with billing."
THEN: COMPLETE
```
```yaml theme={null}
AGENT: Returns_Agent
GOAL: "Handle returns questions"
FLOW:
entry_point: start
steps:
- start
start:
REASONING: false
RESPOND: "I can help with returns."
THEN: COMPLETE
```
## How it works
The welcome message presents choices. The flow gathers `startup_choice`. The handoff conditions use that same field. The specialist agents are included so the compiler and deployment can resolve the targets.
`startup_choice` is declared both at the top level and inside `capture_choice`. Keep only the flow-step `GATHER` if the field is only ever collected inside that step; a top-level declaration is only needed if `HANDOFF` conditions must reference the field before the flow step that collects it has run in a given turn. When in doubt, declare the field once, in the flow step that actually collects it.
Since neither `HANDOFF` example above declares `HISTORY`, each specialist now receives the full conversation history by default (the current platform default when `HISTORY` is omitted).
## Common variations
### Route from a known entry topic
If the channel integration already knows the entry topic, copy it into a session variable during startup and route with that value.
```yaml expandable=true theme={null}
AGENT: Welcome_Context_Router
GOAL: "Route from startup context when the channel passes an entry intent"
HANDOFF:
- TO: Claims_Agent
WHEN: entry_topic == "claims"
PASS: [member_id, entry_topic]
SUMMARY: "User entered from a claims landing page"
- TO: General_Service_Agent
WHEN: entry_topic == "general"
PASS: [member_id, entry_topic]
SUMMARY: "User needs general service"
ON_START:
SET: entry_topic = "claims"
RESPOND: "Welcome. I will get you to the right team."
FLOW:
entry_point: continue
steps:
- continue
continue:
REASONING: false
RESPOND: "Tell me what you need help with."
THEN: COMPLETE
```
```yaml theme={null}
AGENT: Claims_Agent
GOAL: "Handle claims questions"
FLOW:
entry_point: start
steps:
- start
start:
REASONING: false
RESPOND: "I can help with claims."
THEN: COMPLETE
```
```yaml theme={null}
AGENT: General_Service_Agent
GOAL: "Handle general service questions"
FLOW:
entry_point: start
steps:
- start
start:
REASONING: false
RESPOND: "I can help with general service."
THEN: COMPLETE
```
## Verification
Test each startup choice and confirm the expected handoff target. In traces, inspect the startup response, the collected or set routing value, and the handoff event. Also test an unknown choice and confirm the user is asked to clarify instead of being routed incorrectly.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Referencing a route variable that is never set | Button labels and route conditions drift apart. | Gather or set the exact value used in `WHEN`. |
| Showing buttons without a typed fallback | Some users type instead of clicking. | Let the flow gather the same route value from text. |
| Omitting target agents from the project | The supervisor example looks complete by itself. | Define every `TO` agent or state the project-local dependency. |
## Troubleshooting
If no route matches, verify that the variable in `WHEN` has the same name as the gathered or set value. If the compiler reports missing targets, define or import the specialist agents before validating the project.
## Production readiness checklist
* Define every target specialist agent.
* Make every routing variable traceable to `GATHER`, `SET`, channel context, or tool output.
* Add a fallback for unknown or ambiguous choices.
* Pass only the fields the specialist needs.
* Test each welcome option before publishing.
# How to design supervisor routing with priority gates
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/route-with-supervisor-priority
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this pattern when your supervisor must enforce mandatory checks — authentication, fraud screening, regulatory consent, or VIP escalation — before the reasoning model selects a business route.
## Concept
A supervisor with priority gates combines two routing modes in a single HANDOFF block:
* **Deterministic gates** use structured expressions (field comparisons, IS SET checks) and are evaluated first. They enforce hard requirements such as "the user must be authenticated before any account operation."
* **Semantic routes** use quoted natural-language conditions or intent-category comparisons and are evaluated after the gates pass. The reasoning model chooses the best specialist based on the user's intent.
The compiler assigns each HANDOFF entry an evaluation priority from its position in the authored list (first entry = highest priority). The runtime sorts entries by priority and evaluates them top to bottom, selecting the first match. Placing mandatory gates at the top of the HANDOFF block guarantees they are checked before any semantic route.
When a gate hands off with `EXPECT_RETURN: true`, the child agent runs and then returns control. The supervisor can then reprocess the original user intent (`action: resume_intent`) or continue where it left off (`action: continue`).
### History default
When you omit `HISTORY:` on a handoff, the platform default is `full` — the child agent receives the entire parent conversation history. This is important to understand because:
* For temporary gate agents (authentication, compliance), you may want `HISTORY: auto` (summary-first, falling back to the last 5 messages) or `HISTORY: none` to avoid passing unnecessary context.
* For permanent ownership transfers (billing specialist), `full` is usually appropriate so the specialist has complete conversation context.
The supported history strategies are: `full` (default), `auto`, `none`, `summary_only`, and `{ mode: last_n, count: }`.
## Minimal working example
```yaml theme={null}
SUPERVISOR: Priority_Routing_Supervisor
GOAL: "Apply mandatory gates before semantic routing"
INTENTS:
account_help: "Account access, profile, or settings requests."
billing: "Billing, invoice, or payment requests."
support: "General product support requests."
MEMORY:
SESSION:
- is_authenticated
- customer_id
- customer_tier
- account_id
- conversation_summary
HANDOFF:
# Gate 1: Authentication (highest priority, deterministic)
- TO: Authentication_Agent
WHEN: is_authenticated != true
EXPECT_RETURN: true
CONTEXT:
pass: [customer_id]
summary: "Authenticate the customer before handling account or billing data."
history: auto
ON_RETURN:
action: resume_intent
MAP:
is_authenticated: is_authenticated
# Gate 2: VIP escalation (semantic, checked before ordinary routes)
- TO: Priority_Care_Agent
WHEN: "the customer is platinum or strategic and asks for escalation"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, customer_tier, conversation_summary]
summary: "High-priority customer asked for escalation."
# Business routes (intent-based)
- TO: Billing_Agent
WHEN: intent.category == "billing"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, account_id]
summary: "Customer needs billing help."
ON_FAILURE: RESPOND "I was unable to connect you with billing. Let me try again."
- TO: Account_Agent
WHEN: intent.category == "account_help"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id]
summary: "Customer needs account help."
- TO: Support_Agent
WHEN: intent.category == "support"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, conversation_summary]
summary: "Customer needs product support."
# Fallback (lowest priority, catches unmatched messages)
- TO: Support_Agent
WHEN: true
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id]
summary: "No specific route matched. General support fallback."
AGENT: Authentication_Agent
GOAL: "Authenticate customers"
GATHER:
is_authenticated:
TYPE: boolean
PROMPT: "Is the customer authenticated?"
REQUIRED: true
COMPLETE:
- WHEN: is_authenticated IS SET
RESPOND: "Authentication result captured."
AGENT: Priority_Care_Agent
GOAL: "Handle priority escalations"
AGENT: Billing_Agent
GOAL: "Resolve billing requests"
AGENT: Account_Agent
GOAL: "Resolve account requests"
AGENT: Support_Agent
GOAL: "Resolve support requests"
```
## How it works
1. The supervisor receives the user message and keeps ownership until a route matches.
2. Intent categories, runtime context, gathered values, or tool results provide the routing evidence.
3. HANDOFF entries are evaluated in authored order (first = highest priority). The compiler assigns `priority: 1, 2, 3, ...` based on position, and the runtime sorts by priority (lower = first).
4. The first matching WHEN condition wins. Deterministic gates at the top of the list are always checked before semantic or intent-based routes below them.
5. When a handoff fires, the child agent receives the PASS fields, the summary, and conversation history according to the HISTORY strategy. The default strategy is `full` (entire parent history). Use `HISTORY: auto` or `HISTORY: none` when you want the gate agent to receive less context.
6. If the child is temporary (`EXPECT_RETURN: true`), it returns through `ON_RETURN`. The supervisor then either resumes intent routing (`action: resume_intent`) or continues its current flow (`action: continue`).
7. If the child owns the conversation (`EXPECT_RETURN: false`), it remains the active agent permanently.
## Common variations
### Authentication gate before account or billing specialists
Use `EXPECT_RETURN: true` with `ON_RETURN: action: resume_intent` so the supervisor reprocesses the original user intent after authentication completes. Set `HISTORY: auto` to give the auth agent only a summary instead of full conversation history.
```yaml theme={null}
HANDOFF:
- TO: Auth_Gate
WHEN: is_authenticated != true
EXPECT_RETURN: true
CONTEXT:
pass: [session_token]
summary: "Verify user identity before proceeding."
history: auto
ON_RETURN:
action: resume_intent
MAP:
is_authenticated: auth_result
```
### VIP escalation route before ordinary queue routing
Place the VIP check after mandatory gates but before standard business routes. Use a quoted natural-language WHEN condition when the routing evidence comes from business context rather than a single ABL field.
```yaml theme={null}
HANDOFF:
- TO: VIP_Concierge
WHEN: "the customer has a platinum or enterprise tier and is requesting priority service"
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, customer_tier, account_value]
summary: "VIP customer requesting priority service."
```
### Compliance gate with explicit PRIORITY field
When you need to override the default authored-order priority, use the `PRIORITY:` field. Lower values are evaluated first.
```yaml theme={null}
HANDOFF:
- TO: Compliance_Check
WHEN: transaction_amount > 10000
PRIORITY: 0
EXPECT_RETURN: true
CONTEXT:
pass: [transaction_id, transaction_amount, customer_id]
summary: "High-value transaction requires compliance review."
history: none
ON_RETURN:
action: continue
MAP:
compliance_cleared: review_result
- TO: Payment_Agent
WHEN: intent.category == "payment"
PRIORITY: 10
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, transaction_id]
summary: "Process payment request."
```
### CONTEXT shorthand
PASS, SUMMARY, and HISTORY can also appear as direct siblings of the handoff entry instead of nested under CONTEXT. Both forms are equivalent.
```yaml theme={null}
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
PASS: [customer_id, account_id]
SUMMARY: "Customer needs billing help."
HISTORY: full
EXPECT_RETURN: false
```
## Verification
* Parse the ABL and confirm there are no parser errors or warnings.
* Compile the ABL and confirm there are no compiler errors. Review any warnings about undeclared field references — these indicate variables that must be supplied at runtime from context, tool results, or project configuration.
* Test each routing path with a representative utterance:
* For the authentication gate: send any message when `is_authenticated` is not set. Expect the supervisor to route to Authentication\_Agent.
* For the VIP route: send "I need to speak with someone about my enterprise account" when the customer tier is platinum. Expect Priority\_Care\_Agent.
* For intent routes: send "I have a billing question" and confirm the Billing\_Agent is selected.
* For the fallback: send a message that does not match any defined intent. Confirm Support\_Agent is selected as the fallback.
* After the auth gate returns, send a billing-related message and confirm the supervisor re-routes to Billing\_Agent (validating `resume_intent` behavior).
* Inspect routing traces for: selected target, condition result, passed context fields, history strategy applied, and return behavior when `EXPECT_RETURN: true` is used.
## Production readiness checklist
* Every specialist route has a clear owner and a concise context summary.
* All variables referenced in PASS fields are declared in MEMORY:SESSION or are explicitly documented as project-local assumptions that will be populated at runtime.
* Deterministic conditions use declared, gathered, tool-result, runtime, or returned fields.
* Semantic conditions are quoted as natural-language WHEN text.
* Temporary child agents produce every field mapped in ON\_RETURN.
* A fallback route with `WHEN: true` catches unmatched messages explicitly.
* ON\_FAILURE is set on critical routes to handle dispatch failures gracefully.
* The HISTORY strategy is chosen deliberately for each handoff. Use `full` for permanent transfers, `auto` or `none` for temporary gates that should not receive full conversation context.
* Fallback behavior is explicit and does not hide missing intent coverage.
* Routes are tested with representative utterances for each path, including the fallback and the post-gate resume path.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Omitting HISTORY on a gate handoff and leaking full conversation context to a temporary child agent | The default HISTORY strategy is `full`, which passes the entire parent conversation to the child | Set `HISTORY: auto` or `HISTORY: none` on temporary gate agents that do not need the full conversation |
| Making the specialist responsible for checking a mandatory parent gate | It seems simpler to let each child check auth, but this duplicates logic and risks inconsistency | Keep mandatory gates in the supervisor HANDOFF block, above all specialist routes |
| Mapping returned fields that the child agent does not produce | ON\_RETURN MAP references a field name that is not gathered, set, or returned by the child | Ensure the child agent declares or produces every field referenced in ON\_RETURN MAP |
| Using an unquoted expression for externally supplied values | Unquoted conditions are parsed as structured expressions and may produce undefined-variable warnings | Use a quoted natural-language condition when the routing evidence is external business context, or declare the variables in MEMORY:SESSION |
| No fallback route for unmatched messages | All WHEN conditions are specific, so an unmatched message has no explicit path | Add a `WHEN: true` fallback as the last HANDOFF entry |
| PASS fields not declared in MEMORY:SESSION | The compiler resolves PASS fields from session memory for type/description metadata; undeclared fields fall back to type=string | Declare all PASS variables in MEMORY:SESSION for clear typing and documentation |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Gate agent fires on every message even after authentication succeeds | ON\_RETURN MAP does not write back the gate result to the parent session variable | Verify that the MAP key matches the child's output field and the parent's session variable name |
| Semantic route is selected before the deterministic gate | The semantic route appears above the gate in the HANDOFF block (lower authored position = higher priority) | Move mandatory gates to the top of the HANDOFF block |
| Child agent responds without conversation context | HISTORY is set to `none` or `summary_only`, or the summary is empty when using `auto` | Check the HISTORY strategy on the handoff entry; use `full` or provide a non-empty summary for `auto` |
| Compiler warning about undeclared field reference in WHEN condition | The variable in the WHEN expression is not in GATHER, MEMORY:SESSION, or built-in variables | Declare the variable in MEMORY:SESSION, or verify it will be supplied at runtime from context or tool results |
| resume\_intent does not re-route after gate return | The ON\_RETURN action is set to `continue` instead of `resume_intent`, or the MAP does not update the gate variable | Set `action: resume_intent` in ON\_RETURN and verify the MAP updates the condition variable (for example, `is_authenticated`) |
| Unmatched messages produce no response | No fallback route exists in the HANDOFF block | Add a `WHEN: true` fallback entry as the last HANDOFF entry |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route conversations based on user intent](/agent-platform/abl/how-to/route-by-user-intent).
* [How to route unclear requests to a fallback agent](/agent-platform/abl/how-to/route-to-fallback-agent).
# How to run an authentication or profile lookup during welcome
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/run-auth-profile-lookup-on-start
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when the first turn should use authenticated context, such as account status or display name.
## Concept
Authentication and profile lookup are different responsibilities. Authentication establishes who the user is and what context the session can trust. A profile lookup reads business data for that identity. `ON_START` can call a read-only profile tool after the session already has the identity value needed by that tool.
Do not use a welcome message to reveal sensitive information before identity is verified. If the identity is missing, greet generically and ask the user to sign in before account-specific actions.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: Auth_Profile_Lookup_Start_Agent
GOAL: "Load account context during startup before asking the first question"
TOOLS:
get_account_profile(accountId: string) -> {content: string, account_status: string, display_name: string}
description: "Read the account profile for the authenticated user"
side_effects: false
confirm: never
params:
accountId:
description: "Authenticated account identifier supplied by the channel session"
ON_START:
CALL: get_account_profile
WITH:
accountId: session.account_id
AS: accountProfile
RESPOND: "Hello {{accountProfile.display_name}}. Your account is {{accountProfile.account_status}}."
FLOW:
entry_point: service_menu
steps:
- service_menu
service_menu:
REASONING: false
RESPOND: "Would you like billing help, service help, or account help?"
THEN: COMPLETE
```
## How it works
`session.account_id` must already exist in runtime session context. `ON_START` calls the tool, binds the result as `accountProfile`, and then uses the profile fields in the welcome response.
## Common variations
### Guard the welcome when identity is missing
```yaml expandable=true theme={null}
AGENT: Auth_Gated_Welcome_Agent
GOAL: "Welcome authenticated users and avoid exposing account data before identity is present"
ON_START:
BRANCHES:
- IF: session.account_id IS SET
RESPOND: "Welcome. I found your account context."
- ELSE:
RESPOND: "Welcome. Please sign in before account-specific actions."
FLOW:
entry_point: next
steps:
- next
next:
REASONING: false
RESPOND: "How can I help?"
THEN: COMPLETE
```
## Verification
Test two sessions: one with `session.account_id`, and one without it. The authenticated session should call the profile tool and render the account-aware welcome. The unauthenticated session should not expose account-specific data.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using `ON_START` as the authentication step | Profile lookup and authentication are easy to conflate. | Authenticate before account-specific startup copy, then use `ON_START` to read safe profile context. |
| Passing an empty account id to the lookup tool | The channel session did not supply identity. | Add a missing-identity branch and test anonymous sessions. |
| Returning sensitive account details in the greeting | The first turn feels like a convenient place for account state. | Keep the welcome high-level and move sensitive details behind verified flows. |
## Troubleshooting
If the tool input is empty, inspect how the channel or authentication layer populates the session identity. If startup lookup adds latency, keep the welcome generic and move expensive profile calls into the first account-specific flow.
## Production readiness checklist
* Require verified identity before account-specific responses.
* Make profile lookup tools read-only.
* Keep sensitive account details out of the welcome.
* Include a fallback for anonymous or partially authenticated users.
* Trace startup tool calls and profile lookup failures.
# How to switch the active agent during a conversation
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/switch-active-agent
[Back to how-to guides](/agent-platform/abl/how-to/overview#routing-and-multi-agent-orchestration)
Use this when a supervisor should stop answering directly and let a specialist own the next turn, or when a temporary gate must run before the final specialist takes over.
## Concept
Switching the active agent is done through `HANDOFF`. `EXPECT_RETURN: false` transfers ownership permanently to the child specialist — it becomes the active agent for the rest of the conversation. `EXPECT_RETURN: true` creates a temporary child thread that returns to the parent (via `ON_RETURN`), often for authentication or qualification — the supervisor stays the active agent in the long run, just pausing to delegate one sub-task.
The authentication-gate pattern below relies on one subtle but important behavior: on the very first turn, `is_authenticated` doesn't exist yet. The condition `is_authenticated != true` still correctly evaluates to `true` in that case — the runtime treats an undefined variable as not equal to `true` — so the gate fires and routes to `Authentication_Agent` before the variable is ever set. You don't need to special-case "unset" separately from "false."
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Active_Agent_Switch_Supervisor
GOAL: "Switch the active conversation owner to the right child agent"
HANDOFF:
- TO: Authentication_Agent
WHEN: is_authenticated != true
EXPECT_RETURN: true
CONTEXT:
pass: [customer_id]
summary: "Authenticate before account work."
ON_RETURN:
action: resume_intent
MAP:
is_authenticated: is_authenticated
- TO: Account_Service_Agent
WHEN: intent.category == "account_service" AND is_authenticated == true
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, account_id, conversation_summary]
summary: "Account service agent now owns the conversation."
- TO: Billing_Agent
WHEN: intent.category == "billing" AND is_authenticated == true
EXPECT_RETURN: false
CONTEXT:
pass: [customer_id, account_id, conversation_summary]
summary: "Billing agent now owns the conversation."
AGENT: Authentication_Agent
GOAL: "Authenticate customers and return to the supervisor"
GATHER:
is_authenticated:
TYPE: boolean
PROMPT: "Is the customer authenticated?"
REQUIRED: true
COMPLETE:
- WHEN: is_authenticated IS SET
RESPOND: "Authentication result captured."
AGENT: Account_Service_Agent
GOAL: "Own account service after routing"
AGENT: Billing_Agent
GOAL: "Own billing service after routing"
```
`customer_id`, `account_id`, and `conversation_summary` are shown as passed context with no declared source in this example — treat them as project-local assumptions and declare them via `MEMORY` in your actual project. This example also has no fallback route if neither `account_service` nor `billing` matches after authentication — see the fallback-routing HowTo before shipping this pattern as-is.
## How it works
* Before `is_authenticated` is ever set, `is_authenticated != true` evaluates to `true` (undefined is treated as not-equal-to-true), so the very first turn always routes to `Authentication_Agent`.
* `Authentication_Agent` is a temporary child (`EXPECT_RETURN: true`): it gathers `is_authenticated`, completes once that field `IS SET`, and `ON_RETURN` maps its result back into the supervisor's own `is_authenticated` variable and resumes intent routing.
* On the next pass through `HANDOFF`, `is_authenticated == true` now holds, so the compound conditions on `Account_Service_Agent`/`Billing_Agent` can match and transfer ownership permanently (`EXPECT_RETURN: false`).
* None of these conditions are rewritten by the compiler — equality comparisons like `is_authenticated == true` and `intent.category == "account_service"` handle an unset variable correctly on their own (they simply evaluate `false`), so the trace shows your literal authored condition text.
* Since `HISTORY` is omitted from every `HANDOFF`, each child (temporary or permanent) receives the full conversation history by default (the current platform default).
* If you'd rather run the authentication step as a sub-agent call than a full route/return cycle, `DELEGATE` is an alternative to `HANDOFF` + `EXPECT_RETURN: true` for this kind of temporary gate — see the delegate-vs-handoff decision HowTo for the tradeoffs.
## Common variations
* Temporary authentication child returns to supervisor (shown above).
* Final billing or account specialist owns the conversation permanently after the gate passes.
* A return handler maps child output into parent routing state (`MAP` above) so the next routing pass can use it.
## Verification
* Parse and compile the ABL and confirm there are no parser or compiler errors/warnings.
* Test an utterance on a fresh session (no `is_authenticated` set) and confirm it routes to `Authentication_Agent` first.
* After authentication completes, test an account-service and a billing utterance and confirm permanent ownership transfer in each case.
* Inspect the trace for the selected target, the evaluated condition (matches your literal authored text), and the `ON_RETURN`/`MAP` application after the authentication child returns.
## Production readiness checklist
* The auth gate correctly handles the unset case on turn one (no special-casing needed, but verify it in a real trace).
* `MAP` targets a variable the parent actually declares and routes on.
* A fallback route exists for intents that don't match `account_service` or `billing` after authentication — this example doesn't include one.
* Any variable assumed to already exist (like `customer_id` above) has a real declared source in your project.
* Temporary vs. permanent switching is chosen deliberately per route, not by accident.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Trying to switch the active agent by setting a variable | Confusing session state with conversation ownership | Ownership only changes through `HANDOFF`/`DELEGATE`, never by setting a flag |
| Using `EXPECT_RETURN: true` when the child should own the rest of the conversation | Copy-pasting the auth-gate pattern for a different case | Use `EXPECT_RETURN: false` for permanent ownership transfer |
| Passing sensitive or irrelevant context to every child | Reusing one `CONTEXT.pass` list everywhere | Pass only what each specific child needs |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| The conversation never leaves `Authentication_Agent` | `ON_RETURN`/`MAP` isn't correctly writing `is_authenticated` back to the parent, or the child never completes | Inspect the `handoff_return_handler` trace for the actual returned value |
| Every turn re-routes to `Authentication_Agent` even after authenticating | The mapped parent variable name doesn't match what the compound conditions check | Confirm `MAP`'s parent-side key exactly matches the variable used in later `WHEN` conditions |
| A route unexpectedly falls through with no match | No fallback route exists in this example for unmatched intents post-authentication | Add a fallback route (see the fallback-routing HowTo) |
| Child agent has more/less conversation context than expected | `HISTORY` omitted; current default (`full`) applied | Set an explicit `HISTORY` strategy if bounded/summary history is intended |
***
**Related articles:**
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
* [How to route unclear requests to a fallback agent](/agent-platform/abl/how-to/route-to-fallback-agent).
# How to use EXECUTION.pipeline for agent routing, classification, and short-circuiting
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/use-agent-execution-pipeline
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use `EXECUTION.pipeline` when a supervisor or routing agent needs classifier-assisted routing before the normal reasoning path decides what to do.
## Concept
`EXECUTION.pipeline` is the agent execution pipeline, not a Knowledge Base ingestion pipeline, custom operational pipeline, or workflow. It sits in the agent execution path and can classify the user's message, compare the result with known routing targets, short-circuit obvious single-intent requests, and keep ambiguous or risky requests in the normal guided reasoning path.
Use it when routing quality, latency, or tool selection depends on a fast classification pass. Do not use it to orchestrate long-running work. Durable waits, approvals, schedules, and multi-hour work belong in workflows.
The pipeline is disabled by default unless the project-level gate and effective configuration enable it. Agent-level configuration can tune or opt out of behavior, but project configuration still matters for whether the pipeline actually runs.
## Minimal working example
```yaml expandable=true theme={null}
SUPERVISOR: Support_Router
GOAL: "Route support requests using classifier-assisted pipeline decisions"
PERSONA: "Concise support routing supervisor"
EXECUTION:
pipeline:
enabled: true
mode: sequential
model: "gpt-4.1-mini"
shortCircuit:
enabled: true
confidenceThreshold: 0.9
keywordVeto:
enabled: true
keywords: ["refund", "cancel", "fraud"]
intentBridge:
enabled: true
programmaticThreshold: 0.85
guidedThreshold: 0.5
outOfScopeDecline: true
multiIntentSignal: true
HANDOFF:
- TO: Billing_Agent
WHEN: "The user asks about invoices, balances, billing errors, refunds, or payments"
SUMMARY: "User needs billing support"
- TO: Technical_Support_Agent
WHEN: "The user asks about device setup, errors, troubleshooting, or service outage"
SUMMARY: "User needs technical support"
- TO: General_Fallback_Agent
WHEN: "The request is unclear, unsupported, or does not match a specialist"
SUMMARY: "User needs clarification or general support"
```
```yaml theme={null}
AGENT: Billing_Agent
GOAL: "Resolve billing, invoice, refund, and payment questions"
PERSONA: "Billing specialist"
```
```yaml theme={null}
AGENT: Technical_Support_Agent
GOAL: "Troubleshoot device, software, and service issues"
PERSONA: "Technical support specialist"
```
```yaml theme={null}
AGENT: General_Fallback_Agent
GOAL: "Clarify unclear support requests and route users to the right next step"
PERSONA: "General support specialist"
```
## How it works
The classifier first tries to identify the user's intent category. If there is one high-confidence intent with a known category, short-circuiting can let the router choose the target without spending a full reasoning turn.
Short-circuiting is intentionally conservative. It does not fire for low-confidence results, `null` categories, multiple intents, disabled short-circuit config, or keyword-veto matches. Keyword veto is useful for words like `refund`, `cancel`, `fraud`, or sensitive tool names where you want guided reasoning or escalation to inspect the request before direct routing.
`intentBridge` controls what happens after classification:
| Tier | Typical situation | Expected behavior |
| - | - | - |
| Programmatic | Classification is confident enough for deterministic handling | Route or decline according to configured policy |
| Guided | Classification is useful but not certain | Hide or prioritize routing choices while allowing reasoning |
| Autonomous | Classifier is absent, empty, or too uncertain | Let the normal reasoning path decide |
Since none of the `HANDOFF` entries above declare `HISTORY`, each specialist now receives the full conversation history by default (the current platform default when `HISTORY` is omitted).
## Common variations
### Fast single-intent routing
Enable `shortCircuit` for high-volume support routers where users usually ask one clear thing, such as "What is my balance?" or "My device will not start."
### Sensitive keyword veto
Enable `keywordVeto` when certain words should prevent direct short-circuiting. This keeps risky requests in guided reasoning even when the classifier is confident.
### Multi-intent detection
Enable `multiIntentSignal` when users often ask for more than one thing in a single turn, such as "Cancel my subscription and refund my last payment."
### Out-of-scope decline
Use `outOfScopeDecline` when the classifier can confidently detect that a request does not belong to any supported category.
## Verification
* Test a high-confidence single-intent request and confirm the intended specialist is selected.
* Test a low-confidence or broad request and confirm it stays in guided reasoning.
* Test a multi-intent request and confirm it does not short-circuit as a single route.
* Test a veto keyword like `refund` or `fraud` and confirm short-circuiting is blocked.
* Inspect traces for classifier result, confidence, selected category, veto keywords, and final route.
## Production readiness checklist
* Routing categories are distinct and map clearly to handoff targets.
* Fallback target exists for unclear, unsupported, or no-match requests.
* Project-level pipeline configuration is enabled where the agent is deployed.
* Thresholds are tested against real utterances, not only synthetic examples.
* Sensitive keywords are reviewed by product, support, and compliance owners.
* Trace dashboards monitor classifier confidence, short-circuit rate, veto rate, fallback rate, and misroutes.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Treating `EXECUTION.pipeline` as a workflow | Both use the word pipeline | Use workflows for durable orchestration; use `EXECUTION.pipeline` for agent execution routing |
| Enabling short-circuit for ambiguous routing | It looks faster | Keep a fallback path and use conservative confidence thresholds |
| Forgetting project-level enablement | Agent config looks complete | Verify effective runtime pipeline config, not only ABL syntax |
| Missing fallback agent | The classifier cannot always match | Add a fallback route and test no-match requests |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Pipeline config compiles but does not run | Project gate is absent or disabled | Check effective project pipeline config |
| Obvious request does not short-circuit | Confidence below threshold, multiple intents, null category, or veto keyword | Inspect classifier trace data |
| Sensitive request routes too quickly | Keyword veto is missing or disabled | Add sensitive keywords and retest |
| Classifier invents a category | Category is not in known routing set | Confirm the route categories and fallback behavior |
***
**Related articles:**
* [How to configure EXECUTION.pipeline models, modes, thresholds, and fallbacks](/agent-platform/abl/how-to/configure-agent-execution-pipeline).
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
# How to use behavior profiles to change agent behavior by context
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/use-behavior-profiles-by-context
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use behavior profiles when the same agent should keep its core responsibility but change style, constraints, tools, gathering behavior, or channel handling for a specific context.
## Concept
A behavior profile is conditional behavior layered onto an agent. It has a name, a `PRIORITY`, a `WHEN` condition, and one or more behavior changes such as `INSTRUCTIONS`, `CONVERSATION`, `RESPONSE`, `VOICE`, `TOOLS`, `GATHER`, or `FLOW`.
Use behavior profiles for context-sensitive adaptation, not for changing the agent's core job. If the responsibility changes, create another agent. If only the delivery mode or handling style changes, use a behavior profile.
The compiler attaches profiles to the agent IR and execution contract. When profiles contain guidance, that guidance is represented as behavior-profile instructions for execution. Profiles can be inline in an agent or standalone and attached with `USE BEHAVIOR_PROFILE`.
## Minimal working example
```yaml theme={null}
AGENT: Support_Assistant
GOAL: "Resolve support questions with behavior matched to customer context"
PERSONA: "Helpful support assistant"
BEHAVIOR_PROFILE: recovery_mode
PRIORITY: 10
WHEN: sentiment == "frustrated" OR repeat_contact == true
INSTRUCTIONS: |
Acknowledge the customer's prior effort before asking for more information.
Keep the next step concrete and do not list more than two actions.
BEHAVIOR_PROFILE: concise_mode
PRIORITY: 3
WHEN: channel == "sms"
INSTRUCTIONS: "Use short plain-text responses suitable for SMS."
```
## Standalone profile example
```yaml theme={null}
BEHAVIOR_PROFILE: voice_support_mode
PRIORITY: 8
WHEN: channel == "voice"
INSTRUCTIONS: |
Speak in short sentences and confirm important identifiers one digit group at a time.
VOICE:
provider: elevenlabs
speed: 0.95
```
```yaml theme={null}
AGENT: Voice_Support_Assistant
GOAL: "Resolve support questions on voice channels"
PERSONA: "Clear phone support assistant"
USE BEHAVIOR_PROFILE: voice_support_mode
```
## How it works
`PRIORITY` decides which profile should win when multiple profiles match. `WHEN` decides whether a profile applies. Keep conditions based on values your project actually provides, such as channel, sentiment, customer tier, locale, or repeat-contact state.
Inline profiles are useful when the behavior belongs only to one agent. Standalone profiles are useful when several agents share the same channel or customer-context rule.
## Common variations
### Recovery mode
Use a high-priority recovery profile for frustrated users, repeated failures, or repeat contacts.
### Channel mode
Use channel profiles for SMS, voice, WhatsApp, or web when response length, formatting, or speech pacing changes.
### Enterprise customer mode
Use customer-tier profiles when premium or enterprise users require a different tone, faster escalation, or more formal explanations.
## Verification
* Test each `WHEN` condition with a context value that should match and one that should not.
* Confirm higher-priority profiles override lower-priority profiles when both are true.
* Inspect the compiled agent behavior profiles and execution contract for the expected profile count.
* Test channel-specific outputs, especially voice and SMS, in the target channel rather than only in text chat.
## Production readiness checklist
* Every profile has a non-negative integer `PRIORITY`.
* Every profile has a `WHEN` condition.
* Conditions reference context values that are reliably populated.
* Shared standalone profiles are versioned and reviewed because changing one can affect multiple agents.
* Tests cover overlapping profiles, no-match behavior, and channel-specific response constraints.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Using profiles as hidden agents | The profile starts changing the job, not the behavior | Split into another agent when responsibility changes |
| Missing `PRIORITY` or `WHEN` | The profile looks like a simple instruction block | Add both fields to every behavior profile |
| Referencing unavailable context | Conditions never match | Document where each condition value is set |
| Overlapping profiles fight each other | Multiple profiles match the same turn | Use clear priorities and test overlap |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Profile never applies | `WHEN` references an unset value | Confirm the context value exists at runtime |
| Wrong profile wins | Priority order is wrong | Compare all matching profile priorities |
| Shared behavior changed unexpectedly | A standalone profile is reused | Find all agents using the profile before editing |
| Voice behavior is inconsistent | Text profile does not define voice-specific settings | Add `VOICE` or channel-specific instructions |
***
**Related articles:**
* [How to write effective agent goals, personas, instructions, and limitations](/agent-platform/abl/how-to/write-agent-goals-personas-instructions).
* [How to design channel-specific welcome experiences](/agent-platform/abl/how-to/design-channel-specific-welcome).
# How to use ON_START to greet users before the first message
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/use-on-start-welcome
[Back to how-to guides](/agent-platform/abl/how-to/overview#conversation-entry-and-welcome-experience)
Use this pattern when the agent must run startup logic before the user sends the first turn.
## Concept
`ON_START` is the session-start lifecycle handler. It can set session values, call a tool, choose a response branch, return voice or rich payloads, and delegate to another agent. It runs once per initialized runtime session and is idempotent: if the session is already initialized, startup does not run again.
Use `ON_START` for work that belongs to session initialization: greeting, reading channel context, loading a profile, setting counters, or selecting a first-turn experience. Do not use it as a replacement for normal per-turn routing.
## Minimal working example
```yaml expandable=true theme={null}
AGENT: On_Start_Welcome_Agent
GOAL: "Run startup logic before the first user message"
ON_START:
SET: startup_source = session.channel
RESPOND: "Hello. I am ready to help."
FLOW:
entry_point: first_step
steps:
- first_step
first_step:
REASONING: false
RESPOND: "You reached us from {{startup_source}}."
THEN: COMPLETE
```
## How it works
`SET` executes before the response. That means values copied from runtime context can be interpolated into the welcome or into the first flow step. The runtime emits `dsl_set` traces for assignments and `dsl_respond` traces for the startup response.
## Common variations
### Personalize the welcome with startup values
A startup response can include placeholders. The welcome renders with values that `SET` assigns earlier in the same `ON_START` block, so the user sees real session data in the first message rather than a generic greeting.
```yaml expandable=true theme={null}
AGENT: On_Start_Personalized_Agent
GOAL: "Greet the user with values set during startup"
ON_START:
SET: startup_source = session.channel
RESPOND: "Hello. You reached us from {{startup_source}}."
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "How can I help?"
THEN: COMPLETE
```
This works on every channel that triggers a welcome. On Kore Agent Assist, Agent AI triggers it when you select **Use 'Trigger Welcome message' as the first request to Agentic**. For details, see [Agent AI integration](/agent-platform/integrations/agent-ai#choose-first-request-trigger).
### Choose a branch at startup
Branches are evaluated in order. The first matching branch wins. An `ELSE` branch is the default branch.
```yaml expandable=true theme={null}
AGENT: On_Start_Branch_Agent
GOAL: "Choose the first welcome turn from session context"
ON_START:
BRANCHES:
- IF: session.interaction.current.language == "fr"
RESPOND: "Bonjour. Comment puis-je vous aider ?"
- ELSE:
RESPOND: "Hello. How can I help?"
FLOW:
entry_point: ready
steps:
- ready
ready:
REASONING: false
RESPOND: "Ready."
THEN: COMPLETE
```
## Verification
Create sessions with different interaction language values and confirm the selected welcome changes. In traces, check `dsl_on_start_branch` for the matched branch index. Confirm `ON_START` runs once by sending a second message in the same session and checking that no second startup trace is emitted. For a personalized welcome, confirm the first message shows the assigned value rather than the raw placeholder.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Putting `SET` or `CALL` inside a branch | Branches look like mini flows. | Keep side effects at top-level `ON_START`; use branches only to select startup responses. |
| Expecting startup to rerun on every message | `ON_START` is a session initialization hook. | Use normal flow, handoff, or reasoning logic for per-turn behavior. |
| Branching on values that do not exist yet | The first user turn has not happened. | Branch only on runtime context, constants, memory recall, or values set during startup. |
## Troubleshooting
If a branch condition is malformed, the runtime fails closed and falls back to the top-level `ON_START` response when one exists. If no startup response is returned, the flow can still run from its entry point.
## Production readiness checklist
* Put side effects at top-level `ON_START`; branches can choose responses but cannot run `SET`, `CALL`, or `DELEGATE`.
* Use an `ELSE` branch or a top-level fallback response.
* Keep branch conditions based on values that are present before the first user turn.
* Validate traces for `dsl_on_start`, `dsl_set`, and `dsl_on_start_branch`.
# How to write effective agent goals, personas, instructions, and limitations
Source: https://koreai.mintlify.app/agent-platform/abl/how-to/write-agent-goals-personas-instructions
[Back to how-to guides](/agent-platform/abl/how-to/overview#agent-architecture-and-design)
Use this pattern when an agent needs a clear job boundary, a consistent voice, and explicit limits that prevent it from taking work owned by another agent, tool, workflow, or human team.
## Concept
An agent definition needs more than a name. The `GOAL` says what outcome the agent owns. `PERSONA` describes how it should communicate while doing that work. `LIMITATIONS` define what it must not do. Together, these sections give the runtime enough identity to build useful prompts and give your project team a stable contract for routing, testing, and escalation.
Write the goal as an owned responsibility, not a slogan. Write the persona as observable behavior, not a brand adjective list. Write limitations as enforceable boundaries that point to the next action when possible.
## Minimal working example
```yaml theme={null}
AGENT: Refund_Policy_Agent
GOAL: "Answer refund policy questions and explain next steps without creating refund requests"
PERSONA: |
Calm policy specialist.
Uses short answers first, then offers details.
Separates confirmed policy facts from suggested next steps.
LIMITATIONS:
- "Does not approve refunds"
- "Does not change order or payment records"
- "Routes payment disputes to Billing_Specialist"
HANDOFF:
- TO: Billing_Specialist
WHEN: "The user asks to dispute a charge, reverse a payment, or override a refund decision"
PASS: [order_id, customer_id]
SUMMARY: "User needs billing or payment-dispute help"
```
```yaml theme={null}
AGENT: Billing_Specialist
GOAL: "Handle billing and payment-dispute requests"
PERSONA: "Billing specialist"
```
## How it works
The refund agent owns policy explanation, not payment actions. Its limitation is not just "do not process refunds"; it also names the correct boundary for disputes. The `HANDOFF` makes that boundary executable by routing charge disputes to `Billing_Specialist`.
This separation matters in enterprise assistants because customers do not care which internal team owns an issue. They ask naturally. The agent goal and limitations keep the self-service experience helpful while preventing the wrong agent from taking a sensitive action.
Since the `HANDOFF` above omits `HISTORY`, `Billing_Specialist` now receives the full conversation history by default (the current platform default when `HISTORY` is omitted) — set an explicit `HISTORY` strategy if you want bounded or summary-only context instead.
## Common variations
### Short persona
Use a one-line persona for simple specialists:
```yaml theme={null}
AGENT: Billing_Answer_Agent
GOAL: "Answer billing questions without changing account records"
PERSONA: "Concise billing specialist who explains account actions clearly"
```
### Multiline persona
Use a multiline persona when tone, format, and operating style matter:
```yaml theme={null}
AGENT: Identity_Verification_Agent
GOAL: "Verify user identity before sensitive account actions"
PERSONA: |
Direct and reassuring.
Confirms critical identifiers before explaining next steps.
Avoids legal or financial promises.
```
### IDENTITY section
Some projects use `IDENTITY:` to group role, persona, expertise, and limitations. Use it only when the project standard prefers that shape; otherwise, `GOAL`, `PERSONA`, and `LIMITATIONS` are easier to scan.
## Verification
* Test an in-scope request and confirm the agent answers directly.
* Test a boundary request and confirm it hands off, delegates, or declines instead of improvising.
* Test a sensitive request, such as refund approval or payment reversal, and confirm the limitation is respected.
* Inspect the handoff target and passed fields so the receiving agent has enough context.
## Production readiness checklist
* Every agent has one primary responsibility.
* Limitations name prohibited work and the correct next path.
* Routing conditions align with the same boundaries described in limitations.
* Sensitive actions are handled by the right specialist, workflow, tool, or human escalation.
* Test cases cover in-scope, adjacent, out-of-scope, and ambiguous requests.
## Common mistakes
| Mistake | Why it happens | How to avoid it |
| - | - | - |
| Goal is too broad | The agent becomes a catch-all | Use one owned outcome per agent |
| Persona is only branding | It does not guide behavior | Describe response style, confirmation behavior, and tone |
| Limitations have no route | The agent knows what not to do but not what next | Pair important limits with handoff, delegate, escalation, or refusal behavior |
| Limitations conflict with tools | A tool enables an action the agent says it cannot do | Review tools and limitations together |
## Troubleshooting
| Symptom | Likely cause | What to check |
| - | - | - |
| Agent answers outside its scope | Goal or limitations are too broad | Tighten the goal and add boundary tests |
| Agent refuses too much | Limitations are written as general warnings | Rewrite them as specific prohibited actions |
| Handoff happens without useful context | `PASS` fields are missing or unset | Confirm field sources and target-agent needs |
| Two agents claim the same request | Responsibilities overlap | Rewrite goals around ownership, not departments |
***
**Related articles:**
* [How to define clear responsibilities for each enterprise agent](/agent-platform/abl/how-to/define-agent-responsibilities).
* [How to design a supervisor that routes users to specialist agents](/agent-platform/abl/how-to/design-supervisor-routing-agent).
# Agent declaration
Source: https://koreai.mintlify.app/agent-platform/abl/reference/agent-declaration
The agent declaration establishes the identity, metadata, goals, personality, and execution configuration of an ABL agent document. Every `.agent.abl` file must begin with an `AGENT:` declaration.
## AGENT keyword
The `AGENT:` keyword declares the agent's name. This name must be unique within the project and is used as the primary identifier throughout the platform.
```yaml theme={null}
AGENT: Wire_Transfer_Specialist
```
### Naming rules
* Use `PascalCase` with underscores to separate words
* Must start with a letter
* May contain letters, digits, and underscores
* Must be unique within the system
### Examples
```yaml theme={null}
AGENT: Hotel_Search
AGENT: Payment_Processor
AGENT: Customer_Support
AGENT: Fraud_Detection
```
## VERSION
The `VERSION:` directive specifies the semantic version of the agent document. The value must be a quoted string in semver format (`major.minor.patch`).
```yaml theme={null}
VERSION: "2.0.0"
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `VERSION` | `string` (semver) | No | `"1.0.0"` | Document version in `major.minor.patch` format |
The version is stored in the document's metadata (`meta.version`) and is available for compatibility checks, deployment tracking, and audit trails.
### Example
```yaml theme={null}
AGENT: Booking_Agent
VERSION: "3.1.0"
```
## DESCRIPTION
The `DESCRIPTION:` directive provides a human-readable summary of the agent's purpose. It supports both single-line and multi-line (pipe block) formats.
```yaml theme={null}
DESCRIPTION: "Handles flight booking and modifications"
```
```yaml theme={null}
DESCRIPTION: |
Processes outbound wire transfers for retail and commercial banking
customers. Handles domestic (Fedwire) and international (SWIFT)
transfers with full regulatory compliance.
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `DESCRIPTION` | `string` | No | *none* | Human-readable summary of the agent's purpose |
## LANGUAGE
The `LANGUAGE:` directive sets the primary language the agent operates in. The value is a quoted BCP 47 language tag.
```yaml theme={null}
LANGUAGE: "en"
```
```yaml theme={null}
LANGUAGE: "es-EC"
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `LANGUAGE` | `string` | No | *none* | BCP 47 language code (for example. `"en"`, `"es-EC"`, `"fr"`) |
The language directive influences the runtime's language-aware behavior including NLU processing, response generation, and entity recognition.
## Complete declaration property reference
The following table summarizes all agent declaration properties:
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `AGENT` | `string` | **Yes** | -- | Unique agent name in `PascalCase_With_Underscores` |
| `VERSION` | `string` (semver) | No | `"1.0.0"` | Document version |
| `DESCRIPTION` | `string` | No | *none* | Human-readable description |
| `LANGUAGE` | `string` | No | *none* | BCP 47 language code |
***
## Identity and goals
The identity and goals sections define what an agent is trying to achieve, how it presents itself, and what boundaries it operates within. These sections feed directly into the system prompt that guides the LLM's behavior at runtime.
### GOAL
The `GOAL:` section defines the agent's primary objective. It is the only **required** section besides `AGENT:`. The goal drives the LLM's reasoning, determines completion conditions, and provides the core purpose statement included in every system prompt.
```yaml theme={null}
GOAL: "Process the customer's wire transfer request accurately and securely."
```
Multi-line goals use pipe block syntax:
```yaml theme={null}
GOAL: |
Process the customer's outbound wire transfer request accurately and
securely. Collect all required beneficiary and payment details, screen
for sanctions and fraud, enforce daily limits and cut-off windows,
obtain dual authorization when required, and execute the transfer
with a complete audit trail.
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `GOAL` | `string` | **Yes** | -- | The agent's primary objective. Used in system prompt and completion detection. |
#### Goal guidelines
* State the objective from the agent's perspective, not the user's
* Be specific about what the agent should accomplish
* Include success criteria when measurable outcomes exist
* When an agent includes a `FLOW:` section, the goal also serves as context for any steps that have `REASONING: true`
### PERSONA
The `PERSONA:` section describes how the agent should present itself -- its tone, communication style, expertise signaling, and behavioral characteristics. The persona is included in the system prompt to shape the LLM's response style.
```yaml theme={null}
PERSONA: "Friendly and patient customer support representative."
```
```yaml theme={null}
PERSONA: |
Senior wire operations specialist at a large commercial bank — precise
with numbers, methodical about compliance steps, and transparent about
processing times and fees. Never rushes a wire. Confirms every detail
before execution because a released wire cannot be reversed, only recalled.
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `PERSONA` | `string` | No | `""` (empty) | Personality and communication style description |
If omitted, the agent uses a neutral, professional tone determined by the LLM's default behavior.
### LIMITATIONS
The `LIMITATIONS:` section declares explicit boundaries on what the agent cannot or must not do. Each limitation is a quoted string in a list. Limitations are included in the system prompt as hard constraints.
```yaml theme={null}
LIMITATIONS:
- "Cannot process wires to OFAC-sanctioned countries"
- "Cannot override the daily wire transfer limit"
- "Cannot process wires after the daily cut-off"
- "Cannot reverse a wire after execution"
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `LIMITATIONS` | `string[]` | No | `[]` (empty list) | List of explicit boundaries the agent must not cross |
#### Limitation guidelines
* State what the agent **cannot** do, not what it can do
* Be specific -- vague limitations are difficult for the LLM to enforce
* Include the reason or consequence when it helps clarity
* Limitations complement `CONSTRAINTS:` (which are machine-enforced); limitations are LLM-enforced behavioral rules
### INSTRUCTIONS
The `INSTRUCTIONS:` section provides operational guidance that supplements the goal. Instructions are appended to the goal in the system prompt. Use instructions for procedural guidance that does not fit naturally in the goal or persona.
```yaml theme={null}
INSTRUCTIONS: |
Always verify the customer's identity before collecting wire details.
For international wires, confirm the purpose code. If the customer
seems uncertain about the SWIFT/BIC code, recommend they verify
with the beneficiary before proceeding.
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `INSTRUCTIONS` | `string` | No | *none* | Operational guidance appended to the agent's goal |
Instructions and the goal are combined at parse time: the instructions text is appended under an `Instructions:` heading within the goal's description.
### IDENTITY
The `IDENTITY:` section is an alternative, structured format that combines role, persona, expertise, and limitations into a single block. When used, its fields are mapped to the equivalent top-level sections.
```yaml theme={null}
IDENTITY:
role: "Senior wire operations specialist"
persona: "Methodical, precise, and transparent about compliance"
expertise: [wire transfers, compliance screening, FX conversion]
limitations: ["Cannot override daily limits", "Cannot reverse executed wires"]
```
#### IDENTITY field reference
| Field | Type | Required | Default | Maps to |
| - | - | - | - | - |
| `role` | `string` | No | `""` | `GOAL:` (sets the goal description) |
| `persona` | `string` | No | `""` | `PERSONA:` (sets persona description) |
| `expertise` | `string[]` | No | `[]` | Appended to `PERSONA:` as "Expertise: ..." |
| `limitations` | `string[]` | No | `[]` | `LIMITATIONS:` |
#### Mapping behavior
When the parser encounters an `IDENTITY:` block:
1. The `role` value becomes the `GOAL:` description
2. The `persona` value becomes the `PERSONA:` description
3. The `expertise` list is appended to the persona as a comma-separated string
4. The `limitations` list populates `LIMITATIONS:`
You can use either the `IDENTITY:` block or the individual `GOAL:`, `PERSONA:`, `LIMITATIONS:` sections, but if both are present, the individual sections take the values set last in document order.
### Combining identity sections
The recommended approach is to use the individual sections (`GOAL:`, `PERSONA:`, `LIMITATIONS:`) for clarity. The `IDENTITY:` block is available as a compact alternative.
#### Recommended style
```yaml expandable=true theme={null}
AGENT: Hotel_Search
GOAL: |
Help the customer find and book a hotel that matches their preferences,
budget, and travel dates.
PERSONA: |
Enthusiastic travel advisor who knows every major hotel chain and
boutique property. Provides honest comparisons and highlights
trade-offs between price, location, and amenities.
LIMITATIONS:
- "Cannot guarantee room availability — availability changes in real time"
- "Cannot process payments directly — redirects to the booking system"
- "Cannot access loyalty program accounts for other hotel chains"
```
#### Compact style (using IDENTITY)
```yaml theme={null}
AGENT: Hotel_Search
IDENTITY:
role: "Help the customer find and book a hotel matching their preferences"
persona: "Enthusiastic travel advisor with deep hotel industry knowledge"
expertise: [hotel chains, boutique properties, price comparison]
limitations:
- "Cannot guarantee room availability"
- "Cannot process payments directly"
```
### SYSTEM\_PROMPT
The `SYSTEM_PROMPT:` section is an advanced escape hatch that supplies a **complete, custom system
prompt**, replacing the prompt the platform would otherwise generate from `GOAL:`, `PERSONA:`, and
`LIMITATIONS:`.
```yaml theme={null}
SYSTEM_PROMPT: |
You are a wire operations specialist. Follow the bank's compliance
procedures exactly. Never release a wire without dual authorization.
```
When `SYSTEM_PROMPT:` is present, the auto-generated prompt is **not** used — the goal, persona, and limitations are no longer automatically composed into the system prompt. Prefer the structured `GOAL:`/`PERSONA:`/`LIMITATIONS:` sections unless you specifically need full control of the prompt text.
***
## Execution configuration
The `EXECUTION:` section configures runtime behavior for the agent, including LLM model selection, temperature, token limits, timeouts, extended thinking, and per-operation model routing.
### Syntax
```yaml theme={null}
EXECUTION:
model: "claude-sonnet-4-5-20250929"
temperature: 0.3
max_tokens: 4096
tool_timeout: 30000
fallback_model: "claude-haiku-4-5-20251001"
```
All properties are optional. When omitted, the platform applies its built-in defaults.
### Configuration properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `model` | `string` | No | Platform default | Primary LLM model identifier |
| `temperature` | `number` | No | Platform default | Sampling temperature (0.0--1.0). Lower values produce more deterministic output. |
| `top_p` | `number` | No | Platform default | Nucleus-sampling probability mass. |
| `top_k` | `number` | No | Platform default | Top-k sampling cutoff. |
| `frequency_penalty` | `number` | No | Platform default | Penalize tokens by how often they have appeared. |
| `presence_penalty` | `number` | No | Platform default | Penalize tokens that have appeared at all. |
| `seed` | `number` | No | Platform default | Sampling seed for reproducible output (provider-dependent). |
| `stop_sequences` | `string[]` | No | Platform default | Sequences that stop generation. |
| `reasoning_effort` | `string` | No | Platform default | Reasoning effort: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (reasoning models). |
| `verbosity` | `string` | No | Platform default | Output verbosity: `low`, `medium`, or `high` (supported models). |
| `max_tokens` | `number` | No | Platform default | Maximum tokens in LLM response |
| `tool_timeout` | `number` | No | Platform default | Timeout in milliseconds for tool execution |
| `llm_timeout` | `number` | No | Platform default | Timeout in milliseconds for LLM inference calls |
| `session_idle_timeout` | `number` | No | Platform default | Timeout in milliseconds before an idle session expires |
| `max_reasoning_iterations` | `number` | No | Platform default | Maximum reasoning loop iterations for agents without a flow |
| `max_flow_iterations` | `number` | No | Platform default | Maximum step transitions in a flow before forced exit |
| `voice_latency_target` | `number` | No | Platform default | Target latency in milliseconds for voice channel responses |
| `fallback_model` | `string` | No | *none* | Model to use when the primary model is unavailable or errors |
| `enable_thinking` | `boolean` | No | `false` | Enable extended thinking (Anthropic Claude models) |
| `thinking_budget` | `number` | No | Platform default | Token budget allocated to extended thinking |
| `token_budget` | `number` | No | *none* | Cumulative LLM token budget for a single execution/session; primarily used to bound debug/test runs |
| `compaction_threshold` | `number` | No | Platform default | Context-usage ratio (0.0--1.0) that triggers auto-compaction |
| `compaction` | `object` | No | Platform default | Fine-grained compaction policy overrides. See [Compaction policy overrides](#compaction-policy-overrides). |
| `inline_gather` | `boolean` | No | `false` | When `true`, gather fields are collected inline during conversation rather than as a separate phase |
| `concurrency` | `string` | No | `serial` | Message-processing strategy: `serial`, `preemptive`, or `parallel`. See [Message concurrency](#message-concurrency). |
| `max_queue_depth` | `number` | No | `10` | Maximum pending messages held in the per-session queue. |
| `max_concurrent_messages` | `number` | No | Platform default | Maximum concurrent message executions when `concurrency: parallel`. |
| `conversation_history_window` | `number` | No | Platform default | Number of prior conversation messages to include in the LLM context. |
| `voice` | `object` | No | *none* | Agent-level voice (TTS) configuration. See [Voice configuration](/agent-platform/abl-reference/rich-content#voice-configuration). |
| `pipeline` | `object` | No | disabled | Pre-classification / tool-filter pipeline (supervisors). See [Multi-Agent and Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#execution-pipeline-pre-classification). |
### Extended thinking
Extended thinking allows Claude models to perform internal reasoning before producing a response. This is useful for complex multi-step reasoning tasks where the agent needs to plan before acting.
```yaml theme={null}
EXECUTION:
model: "claude-sonnet-4-5-20250929"
enable_thinking: true
thinking_budget: 8000
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `enable_thinking` | `boolean` | No | `false` | Enable the extended thinking capability |
| `thinking_budget` | `number` | No | Platform default | Maximum tokens the model can use for internal reasoning |
When enabled, the model uses a portion of its context window for internal chain-of-thought reasoning before producing the visible response. The `thinking_budget` controls how many tokens are allocated to this internal reasoning.
### Context compaction
As conversations grow, the context window fills with message history. Context compaction automatically summarizes older messages when the context usage exceeds a threshold, preventing context-window overflow.
```yaml theme={null}
EXECUTION:
compaction_threshold: 0.75
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `compaction_threshold` | `number` | No | Platform default | Ratio (0.0--1.0) of context usage that triggers compaction |
When the ratio of used tokens to available tokens exceeds this threshold, the runtime compacts older conversation history into a summary, freeing space for new messages while preserving essential context.
#### Compaction policy overrides
For fine-grained control, the `compaction:` sub-block overrides how three categories of context are compacted. This is advanced tuning — most agents only need `compaction_threshold`.
```yaml expandable=true theme={null}
EXECUTION:
compaction:
tool_results:
strategy: structured # none | truncate | structured | summarize
max_chars: 2000
keep_recent: 3
prior_turns:
strategy: compact # none | placeholder | compact | summarize
assistant_preview_chars: 200
prompt_context:
projection_strategy: summary # preview | summary | metadata
value_max_chars: 500
total_max_chars: 4000
```
| Sub-block | Purpose | `strategy` values |
| - | - | - |
| `tool_results` | How historical tool-call results are compacted | `none`, `truncate`, `structured`, `summarize` |
| `prior_turns` | How older conversation turns are compacted | `none`, `placeholder`, `compact`, `summarize` |
| `prompt_context` | How injected prompt-context values are projected | `projection_strategy`: `preview`, `summary`, `metadata` |
Each sub-block accepts additional character-budget and retention keys (for example. `max_chars`,
`keep_recent`, `assistant_preview_chars`, `value_max_chars`, `total_max_chars`).
### Message concurrency
The `concurrency` property controls how a session handles multiple inbound messages.
```yaml theme={null}
EXECUTION:
concurrency: preemptive # serial | preemptive | parallel
max_queue_depth: 10
max_concurrent_messages: 3
```
| Value | Behavior |
| - | - |
| `serial` | (Default) Process one message at a time; additional messages queue. |
| `preemptive` | A new message can interrupt/replace in-flight processing of a superseded turn. |
| `parallel` | Process up to `max_concurrent_messages` messages concurrently. |
`max_queue_depth` (default `10`) bounds how many messages wait in the per-session queue.
`concurrency`, `max_queue_depth`, `max_concurrent_messages`, and the `EXECUTION.voice` block are supported in the `.agent.abl` format. The `.agent.yaml` format does not yet parse these keys.
### Per-operation model routing
The `models:` sub-block within `EXECUTION:` allows you to route different operations to different LLM models. This enables cost optimization by using faster, cheaper models for simple tasks and more capable models for complex reasoning.
```yaml theme={null}
EXECUTION:
model: "claude-sonnet-4-5-20250929"
fallback_model: "claude-haiku-4-5-20251001"
models:
extraction: "claude-haiku-4-5-20251001"
response_gen: "claude-sonnet-4-5-20250929"
reasoning: "claude-sonnet-4-5-20250929"
coordination: "claude-sonnet-4-5-20250929"
```
The `models:` block maps operation names to model identifiers. The `model` property serves as the default for any operation not explicitly listed.
#### Recognized operation names
| Operation | Description |
| - | - |
| `extraction` | Extracting structured data from user messages (for example. gather field extraction) |
| `validation` | Validating extracted or gathered values |
| `tool_selection` | Selecting which tool to call (also used by the classification pipeline) |
| `response_gen` | Generating user-facing responses |
| `summarization` | Summarizing conversation history during compaction |
| `reasoning` | Complex reasoning, planning, and decision-making |
| `realtime_voice` | Low-latency voice-channel turns |
| `coordination` | Multi-agent coordination and routing decisions |
| `embedding` | Embedding generation |
| `title` | Conversation/title generation |
Any other operation name is accepted by the parser but only resolved at runtime if the platform recognizes it.
`EXECUTION.compaction.model` is deprecated — use `EXECUTION.models.summarization` instead to set the model used when summarizing history.
### Voice coordinator policy
For realtime voice agents, `EXECUTION.voice.coordinator_policy` sets developer-controlled delivery preferences — tone, pace, brevity, and accessibility of spoken output. It shapes how the voice coordinator speaks; the semantic content still comes from the agent's goal and prompts.
```yaml theme={null}
EXECUTION:
voice:
coordinator_policy:
language: en-US
tone: warm # neutral | warm | formal | energetic
pace: slow # slow | normal | fast
brevity: concise # concise | standard
interruption: acknowledge_then_resume # resume_latest | acknowledge_then_resume
greeting_style: brief # brief | warm | none
accessibility:
confirm_critical_values: true
spell_sensitive_values: true
```
| Field | Type | Description |
| - | - | - |
| `language` | `string` | BCP-47 language tag for delivery (for example, `en-US`). |
| `tone` | `"neutral"` \| `"warm"` \| `"formal"` \| `"energetic"` | Speaking tone. |
| `pace` | `"slow"` \| `"normal"` \| `"fast"` | Speaking pace. |
| `brevity` | `"concise"` \| `"standard"` | How terse responses should be. |
| `interruption` | `"resume_latest"` \| `"acknowledge_then_resume"` | How to resume after the caller interrupts. |
| `greeting_style` | `"brief"` \| `"warm"` \| `"none"` | Opening-greeting style. |
| `accessibility.confirm_critical_values` | `boolean` | Read back critical values (amounts, dates) for confirmation. |
| `accessibility.spell_sensitive_values` | `boolean` | Spell out sensitive values (codes, IDs) character by character. |
Every key also accepts its camelCase spelling (`greetingStyle`, `confirmCriticalValues`, `spellSensitiveValues`). This delivery policy is separate from the per-output [scoped `barge_in`](/agent-platform/abl/reference/flow#scoped-barge-in) policy.
### Complete execution example
```yaml expandable=true theme={null}
EXECUTION:
model: "claude-sonnet-4-5-20250929"
temperature: 0.1
max_tokens: 4096
max_reasoning_iterations: 8
tool_timeout: 30000
llm_timeout: 60000
session_idle_timeout: 900000
fallback_model: "claude-haiku-4-5-20251001"
enable_thinking: true
thinking_budget: 4000
compaction_threshold: 0.8
models:
extraction: "claude-haiku-4-5-20251001"
response_gen: "claude-sonnet-4-5-20250929"
reasoning: "claude-sonnet-4-5-20250929"
coordination: "claude-sonnet-4-5-20250929"
```
***
## Complete example
```yaml expandable=true theme={null}
AGENT: Wire_Transfer_Specialist
VERSION: "2.0.0"
DESCRIPTION: |
Processes outbound wire transfers for retail and commercial banking
customers. Handles domestic (Fedwire) and international (SWIFT)
transfers with full regulatory compliance.
LANGUAGE: "en"
GOAL: |
Process the customer's outbound wire transfer request accurately
and securely.
PERSONA: |
Senior wire operations specialist at a large commercial bank — precise
with numbers, methodical about compliance steps, and transparent about
processing times and fees.
LIMITATIONS:
- "Cannot process wires to OFAC-sanctioned countries"
- "Cannot override the daily wire transfer limit"
- "Cannot reverse a wire after execution"
EXECUTION:
model: "claude-sonnet-4-5-20250929"
temperature: 0.1
max_tokens: 4096
tool_timeout: 30000
fallback_model: "claude-haiku-4-5-20251001"
```
***
**Related articles:**
* [Language overview](/agent-platform/abl-reference/language-overview) -- file structure, syntax rules, and auto-detection
* [Tools](/agent-platform/abl-reference/tools) -- tool-level timeout configuration
* [FLOW](/agent-platform/abl-reference/flow) -- per-step GOAL overrides and `MAX_TURNS` for reasoning steps
# Behavior profiles
Source: https://koreai.mintlify.app/agent-platform/abl/reference/behavior-profiles
Behavior profiles are standalone ABL documents. Agents attach them with `USE BEHAVIOR_PROFILE: `, and the compiler attaches the matching profile IR to that agent.
## File contract
Behavior profile files belong in `behavior_profiles/` and should use the `.behavior_profile.abl` suffix:
```text theme={null}
behavior_profiles/voice_vip.behavior_profile.abl
```
The manifest records them under `behavior_profiles`, while `layers_included` still uses `core`. Do'nt add `behavior_profiles` to `layers_included`.
The manifest entry is a pointer, not the profile definition. Import preview compiles the actual file at `behavior_profiles..path`. A package that only lists a profile in `project.json` but omits the referenced `.abl` file doesn't create the profile; agents with `USE BEHAVIOR_PROFILE: ` will fail resolution with a missing profile diagnosis.
```json theme={null}
{
"format_version": "2.0",
"layers_included": ["core"],
"behavior_profiles": {
"voice_vip": {
"name": "voice_vip",
"path": "behavior_profiles/voice_vip.behavior_profile.abl",
"priority": 100,
"when_summary": "channel == voice",
"used_by": ["Support_Agent"]
}
}
}
```
## Grammar
```yaml expandable=true theme={null}
BEHAVIOR_PROFILE: voice_vip
PRIORITY: 100
WHEN: channel == "voice"
INSTRUCTIONS: |
Speak concisely and confirm one thing at a time.
CONVERSATION:
speaking:
tone: warm
max_sentences: 2
VOICE:
instructions: "Use a calm voice with clear pauses."
TOOLS:
HIDE: [internal_debug_tool]
GATHER:
FIELD_OVERRIDES:
account_id:
prompt: "Please say or enter your account number."
```
Required declarations:
| Declaration | Required | Notes |
| - | - | - |
| `BEHAVIOR_PROFILE:` | yes | Case-sensitive profile name |
| `PRIORITY:` | yes | Non-negative integer. Higher priority wins when profiles conflict. |
| `WHEN:` | yes | CEL expression evaluated against context (`WHEN: true` for a default profile). |
## Profile blocks
Beyond the required declarations, a profile can override several aspects of agent behavior. All
blocks are optional.
### CONSTRAINTS
Adds business-rule constraints while the profile is active (a list of strings).
```yaml theme={null}
CONSTRAINTS:
- "Never reveal internal tool names."
- "Always confirm amounts above $500."
```
### RESPONSE
Shapes response rendering.
| Sub-key | Type | Description |
| - | - | - |
| `max_buttons` | `number` | Maximum interactive buttons to render. |
| `fallback_format` | `string` | Format to fall back to (for example. `text`). |
| `media_types` | `string[]` | Allowed media types. |
| `max_response_length` | `number` | Maximum response length. |
### VOICE
Voice (TTS) overrides while the profile is active. Accepts the full
[voice configuration](/agent-platform/abl-reference/rich-content#voice-configuration)
— `instructions`, `ssml`, `plain_text`, `provider`, `voice_id`, `speed`, `pace`, `temperature`, and
`auto_tts_language_switch`.
### TOOLS
`HIDE:` removes tools from the agent's tool set; `ADD:` injects new tool definitions while the
profile is active.
```yaml theme={null}
TOOLS:
HIDE: [internal_debug_tool]
ADD:
priority_callback(reason: string) -> {ticket_id: string}
description: "Open a VIP priority callback ticket"
PARAMETERS:
- reason: string
```
### CONVERSATION
`CONVERSATION:` is also a valid **top-level agent section** — an agent can declare it directly
(`doc.conversation`) with the exact same grammar shown here. Use the profile form to apply
conversation overrides conditionally (via the profile's `WHEN`), or the top-level form to set an
agent's baseline conversation behavior.
Conversation-style overrides, organized into three sub-sections:
* **`speaking`** — output style: `style`, `tone`, `emotion`, `pace`, `variety`, `max_sentences`, `one_thing_at_a_time`, `language_policy` (`interaction_context` / `agent_default` / `fixed`), `fixed_language`, `tool_lead_in`, `tool_results`, `readback`, `handoffs`, `phrases_ref`, `pronunciations_ref`.
* **`listening`** — input handling: `barge_in`, `backchannels`, `on_pause`, `on_overlap`, `on_unclear_audio`, `on_self_correction`, `use_audio_cues`, and the voice sub-blocks `on_no_input`, `on_caller_backchannel`, `on_hold`, and `barge_in_continuation` (see [Voice turn-taking](#voice-turn-taking)).
* **`interaction`** — dialogue behavior: `answer_shape`, `detail`, `initiative`, `grounding`, `clarification`, `confirmation`, `uncertainty`, `empathy`, `repair`, `context`, `closure`, `assumption_handling`, `flow_mode`, and more.
```yaml theme={null}
CONVERSATION:
speaking:
tone: warm
max_sentences: 2
one_thing_at_a_time: true
listening:
barge_in: allow
interaction:
clarification:
max_questions: 1
```
#### Voice turn-taking
For voice/telephony channels, four `listening` sub-blocks control turn-taking behavior:
**`on_no_input`** — handle caller silence.
| Field | Type | Description |
| - | - | - |
| `enabled` | `boolean` | Toggle no-input handling. |
| `mode` | `"static"` , `"dynamic"` | Static prompts vs. LLM-generated re-prompts. |
| `timeout_ms` | `number` | Silence duration before triggering. |
| `relaxed_interval_ms` | `number` | Extended interval between retries. |
| `max_retries` | `number` | Maximum no-input retries. |
| `on_exhausted` | `"hangup"` , `"transfer"` , `"custom"` | Action when retries run out. |
| `exhausted_message` | `string` | Message when retries are exhausted. |
| `order` | `"sequence"` , `"random"` | Prompt play order. |
| `play_style` | `"looped"` , `"once"` , `"non_looped"` | Whether prompts repeat. |
| `messages` | `object[]` | Static re-prompt texts. |
| `generation` | `object` | Dynamic prompt generation (`mode: dynamic`): `enabled`, `model_source`, `model_id`, `prompt_ref`, `max_output_tokens`. |
**`on_caller_backchannel`** — recognize caller acknowledgements ("uh-huh", "okay") without treating them as a turn: `enabled`, `phrases`, `max_words`.
`max_duration_ms` on `on_caller_backchannel` is accepted but has **no effect** — the voice gateway cannot enforce a back-channel duration cap, so back-channels are matched on the phrase list alone. Authoring it now emits a compile warning (`CONVERSATION_BACKCHANNEL_DURATION_CAP_IGNORED`); remove it.
**`on_hold`** — handle caller hold requests ("hold on a sec"): `enabled`, `phrases`, `phrases_by_locale`, `grace_ms`, `max_holds`, `suppress_response`. `phrases_by_locale` is a `{ : string[] }` map of additional hold phrases selected by the caller's detected locale; its phrases are unioned with the language-neutral `phrases` list rather than replacing them. Locale keys are canonicalized at parse time (`en_us` → `en-US`).
```yaml expandable=true theme={null}
CONVERSATION:
listening:
on_no_input:
enabled: true
mode: static
timeout_ms: 5000
max_retries: 2
on_exhausted: transfer
messages:
- text: "Are you still there?"
on_caller_backchannel:
enabled: true
phrases: ["uh-huh", "okay", "go on"]
on_hold:
enabled: true
phrases: ["hold on", "give me a sec"]
phrases_by_locale:
es-MX: ["un momento", "espérame un segundo"]
fr-CA: ["un instant", "attends une seconde"]
grace_ms: 15000
```
`phrases_by_locale` is currently authored only in the `.agent.yaml` format (`CONVERSATION.listening.on_hold.phrases_by_locale`); the legacy `.agent.abl` parser does not yet accept this key.
**`barge_in_continuation`** — when a caller barges in over the bot and the speech recognizer segments their continuous speech into multiple phrase-finals (for example "change to 2 years" then, without a real pause, "recalculate the EMI"), hold each eligible final briefly and merge a following one into a single turn instead of committing each phrase as its own turn.
| Field | Type | Range | Description |
| - | - | - | - |
| `enabled` | Boolean | — | Main switch. Defaults to `on` when the block is omitted entirely. |
| `window_ms` | Number | 300–2000 | Silence window held after a final before committing it, waiting for a possible continuation. |
| `max_hold_ms` | Number | 500–8000 | Absolute ceiling on how long one turn may be held, regardless of how many finals merge into it. |
| `max_merges` | Number | 1–20 | Maximum number of finals folded into one committed turn. |
| `max_chars` | Number | 1–20000 | Maximum merged-transcript length before forcing a commit. |
```yaml theme={null}
CONVERSATION:
listening:
barge_in: allow
barge_in_continuation:
enabled: true
window_ms: 700
max_hold_ms: 4000
max_merges: 4
max_chars: 2000
```
`barge_in_continuation` refines what happens to an *already-allowed* interruption's speech segmentation — it's a sibling of, not a replacement for, the scalar `barge_in: allow`/`disallow` policy above, and distinct from the per-output scoped `BARGE_IN` covered in [Scoped barge-in](/agent-platform/abl-reference/flow#scoped-barge-in). A project-level default can be overlaid beneath this per-agent block.
### GATHER
Overrides gather behavior. Top-level keys `validation_style` and `confirmation`, plus a
`FIELD_OVERRIDES:` block (note the uppercase keyword) that adjusts individual fields.
```yaml theme={null}
GATHER:
validation_style: lenient
confirmation: always
FIELD_OVERRIDES:
account_id:
prompt: "Please say or enter your account number."
required: true
skip: false
validation: "len(account_id) == 10"
extraction_hints: ["Read digits individually on voice"]
```
### FLOW
Overrides the agent's flow while the profile is active.
| Sub-keyword | Shape | Description |
| - | - | - |
| `SKIP` | `string[]` | Step names to skip. |
| `REPLACE` | `string` | Replace the flow with a named flow. |
| `OVERRIDES` | per-step `{ respond?, transition?, VOICE?, RICH_CONTENT? }` | Override specific steps (alias `OVERRIDE`). |
| `INSERTIONS` | list of `- BEFORE: ` / `- AFTER: ` + step body | Insert steps relative to existing ones. |
```yaml theme={null}
FLOW:
SKIP: [upsell_step]
OVERRIDES:
greeting:
respond: "Welcome back — I'll keep this quick."
INSERTIONS:
- BEFORE: payment
REASONING: false
RESPOND: "Confirming your VIP discount first."
```
An **inline** `BEHAVIOR_PROFILE:` declared inside an agent file (rather than as a standalone
document) supports only `PRIORITY`, `WHEN`, `INSTRUCTIONS`, and `CONVERSATION`. The other blocks
(`CONSTRAINTS`, `RESPONSE`, `VOICE`, `TOOLS`, `GATHER`, `FLOW`) are available only in standalone
profile files.
## Agent attachment
```yaml theme={null}
AGENT: Support_Agent
GOAL: "Resolve customer support requests"
USE BEHAVIOR_PROFILE: voice_vip
```
The attached profile name must match the `BEHAVIOR_PROFILE:` header, not just the filename. Both
`USE BEHAVIOR_PROFILE:` (space) and `USE_BEHAVIOR_PROFILE:` (underscore) forms are accepted.
## Compile and import behavior
During import preview, the platform:
1. Reads `project.json` and `abl.lock`.
2. Categorizes `behavior_profiles/.behavior_profile.abl` as part of the `core` layer.
3. Parses the profile with the canonical ABL parser.
4. Compiles profiles before agents.
5. Resolves every `USE BEHAVIOR_PROFILE` reference against compiled profile names.
6. Reports missing or malformed profiles as import/compiler issues.
Common fixes:
| Symptom | Fix |
| - | - |
| `Unknown layer "behavior_profiles" in layers_included` | Remove it from `layers_included`; keep `core` and the manifest map. |
| Manifest has `behavior_profiles` but preview adds none | Include the referenced `behavior_profiles/.behavior_profile.abl` file. |
| `Missing BEHAVIOR_PROFILE: header` | Start the profile file with `BEHAVIOR_PROFILE: `. |
| `BEHAVIOR_PROFILE requires a PRIORITY declaration` | Add `PRIORITY: `. |
| `BEHAVIOR_PROFILE requires a WHEN declaration` | Add `WHEN: `, or `WHEN: true` for a default profile. |
| Agent references a missing `profile_use` target | Add the standalone profile file or remove the `USE` reference. |
# Data types and utilities
Source: https://koreai.mintlify.app/agent-platform/abl/reference/data-types-and-utilities
This page documents the ABL type system, lookup tables for reference-based validation, and attachment handling for file and media uploads.
***
## Data types
ABL has a type system used for variable declarations, gather field types, tool parameter signatures, and runtime validation. Types are divided into primitives and complex types.
### Primitive types
Primitive types represent single scalar values.
| Type | Description | Example values |
| - | - | - |
| `string` | Unicode text of arbitrary length. | `"hello"`, `"USD"`, `""` |
| `number` | IEEE 754 double-precision floating point. | `42`, `3.14`, `-1`, `0` |
| `boolean` | True or false. | `true`, `false` |
| `date` | Calendar date without time. | `"2024-03-15"` |
| `datetime` | Date with time and timezone. | `"2024-03-15T10:30:00Z"` |
#### String
Strings are the most common type in ABL. They are used for text input, identifiers, messages, and any unstructured data.
```yaml theme={null}
GATHER:
customer_name:
prompt: "What is your name?"
type: string
required: true
```
#### Number
Numbers represent all numeric values, including integers and floating-point numbers. ABL does not distinguish between integer and float types.
```yaml theme={null}
GATHER:
amount:
prompt: "How much would you like to transfer?"
type: number
required: true
validate: min(1)
```
#### Boolean
Booleans represent true/false values. In conditions, the following values are treated as falsy: `false`, `0`, `""`, `"false"`, `null`, `undefined`, empty arrays, and empty objects. Everything else is truthy.
```yaml theme={null}
MEMORY:
session:
- customer_verified
TYPE: boolean
INITIAL: false
```
#### Date
Dates represent calendar dates without a time component. They're stored as ISO 8601 format (`YYYY-MM-DD`).
```yaml theme={null}
GATHER:
departure_date:
prompt: "When would you like to depart?"
type: date
required: true
```
#### Datetime
Datetimes include both date and time with timezone information. They're stored in ISO 8601 format.
```yaml theme={null}
MEMORY:
session:
- session_start_time
TYPE: datetime
INITIAL: NOW()
```
### Complex types
Complex types represent structured or composite values.
#### array\
An ordered collection of elements, optionally typed by item type.
```yaml theme={null}
# In type definitions
type: array # Array of strings
type: array # Array of numbers
type: array # Array of objects
```
In memory declarations:
```yaml theme={null}
MEMORY:
session:
- cart_items
TYPE: array
INITIAL: []
```
##### Array type definition (IR)
```yaml theme={null}
{ kind: 'array', itemType: TypeDefinition }
```
| Property | Type | Description |
| - | - | - |
| `kind` | `'array'` | Discriminant for array type. |
| `itemType` | `TypeDefinition` | Type of each element in the array. |
#### object `<{...}>`
A structured record with named, typed fields.
```yaml theme={null}
# In type definitions
type: object<{name: string, age: number, active: boolean}>
```
In tool return types:
```yaml theme={null}
TOOLS:
get_user(id: string) -> {name: string, email: string, role: string}
```
##### Object type definition (IR)
```yaml theme={null}
{ kind: 'object', properties: Record }
```
| Property | Type | Description |
| - | - | - |
| `kind` | `'object'` | Discriminant for object type. |
| `properties` | `Record` | Map of field names to their types. |
#### enum\<\[...]>
A constrained set of allowed string values.
```yaml theme={null}
GATHER:
transfer_type:
prompt: "Is this domestic or international?"
type: string
validate: enum(domestic, international)
```
##### Enum type definition (IR)
```yaml theme={null}
{ kind: 'enum', values: string[] }
```
| Property | Type | Description |
| - | - | - |
| `kind` | `'enum'` | Discriminant for enum type. |
| `values` | `string[]` | Allowed values. |
#### union\<\[...]>
A value that can be one of several types. Useful for fields that accept different formats.
```yaml theme={null}
# In type definitions
type: union<[string, number]> # Either a string or a number
```
##### Union type definition (IR)
```yaml theme={null}
{ kind: 'union', types: TypeDefinition[] }
```
| Property | Type | Description |
| - | - | - |
| `kind` | `'union'` | Discriminant for union type. |
| `types` | `TypeDefinition[]` | Possible types for the value. |
#### nullable\
Wraps another type to indicate it can also be `null`.
```yaml theme={null}
# In type definitions
type: nullable # String or null
type: nullable # Number or null
```
##### Nullable type definition (IR)
```yaml theme={null}
{ kind: 'nullable', innerType: TypeDefinition }
```
| Property | Type | Description |
| - | - | - |
| `kind` | `'nullable'` | Discriminant for nullable type. |
| `innerType` | `TypeDefinition` | The underlying type that can also be null. |
### Type definitions in ABL contexts
#### Variable declarations
In `MEMORY:` session and persistent variable declarations, use the `TYPE` property:
```yaml theme={null}
MEMORY:
session:
- order_total
TYPE: number
INITIAL: 0
- items
TYPE: array
- customer_profile
TYPE: object
```
#### Gather fields
Gather fields use the `type` property with primitive type names:
```yaml expandable=true theme={null}
GATHER:
name:
prompt: "What is your name?"
type: string
required: true
age:
prompt: "How old are you?"
type: number
required: true
departure_date:
prompt: "When do you depart?"
type: date
required: true
```
#### Tool parameters
Tool parameters declare types inline in the signature:
```yaml theme={null}
TOOLS:
search_flights(origin: string, destination: string, date: date, passengers: number) -> {flights: {id: string, price: number, departure: datetime}[], total: number}
```
Supported parameter types:
| Type | Description |
| - | - |
| `string` | Text parameter. |
| `number` | Numeric parameter. |
| `boolean` | Boolean parameter. |
| `date` | Date parameter. |
| `datetime` | Datetime parameter. |
| `object` | Structured object. |
| `string[]` | Array of strings. |
| `number[]` | Array of numbers. |
#### Tool return types
Tool return types use object literal notation with field names and types:
```yaml theme={null}
-> {field1: type1, field2: type2, nested: {sub_field: type3}[]}
```
The `ToolReturn` structure supports nested objects and arrays:
| Property | Type | Description |
| - | - | - |
| `type` | `string` | Type name (`string`, `number`, `object`, etc.). |
| `fields` | `Record` | Sub-fields (for `object` type). |
| `items` | `ToolReturn` | Item type (for array types). |
| `optional` | `boolean` | Whether this field is optional in the response. |
### Variable sources
Variables in ABL have a source that indicates their origin:
| Source | Description |
| - | - |
| `system` | Set by the runtime (for example, session ID, timestamp). |
| `user` | Set by user input (gather fields, direct input). |
| `agent` | Set by agent logic (SET assignments, tool results). |
| `computed` | Derived from other variables via expressions. |
```yaml theme={null}
STATE:
user:
language:
type: string
source: user
system:
session_id:
type: string
source: system
```
### Type coercion at runtime
The runtime applies type coercion in these contexts:
| Context | Behavior |
| - | - |
| Comparison | Strings are parsed to numbers for numeric comparisons. Booleans become 0/1. |
| Assignment | Values are stored as-is. No implicit conversion on write. |
| Function calls | Math functions coerce string arguments to numbers. String functions coerce null to `""`. |
| Tool parameters | Values are coerced to match the declared parameter type before sending. |
| Gather validation | User input is validated and coerced to the declared gather field type. |
For detailed coercion rules in expression evaluation, see [Expressions & functions -- Type coercion rules](/agent-platform/abl-reference/expressions-and-functions#type-coercion-rules).
### Complete TypeDefinition reference
The full `TypeDefinition` type is a union of primitive type names and complex type objects:
```yaml expandable=true theme={null}
TypeDefinition =
| 'string'
| 'number'
| 'boolean'
| 'date'
| 'datetime'
| { kind: 'array', itemType: TypeDefinition }
| { kind: 'object', properties: Record }
| { kind: 'enum', values: string[] }
| { kind: 'union', types: TypeDefinition[] }
| { kind: 'nullable', innerType: TypeDefinition }
```
`TypeDefinition` is the **structural** type algebra used for `MEMORY:` variables and tool signatures. GATHER fields and [`ENTITIES:`](/agent-platform/abl-reference/nlu) entries use a broader **entity type** set that additionally includes `text`, `free_text`, `integer`, `float`, `currency`, `email`, `phone`, `enum`, `pattern`, and `location`. Those extra type names are not part of `TypeDefinition`.
***
## Lookup tables
Lookup tables provide reference-based validation for gather fields and expressions. They define sets of valid values that the runtime uses to validate user input, suggest corrections, and perform fuzzy matching. The `LOOKUP_TABLES:` block declares named tables with their data source and matching configuration.
### Overview
ABL supports three lookup table sources:
* **Inline** -- static values defined directly in the ABL file.
* **Collection** -- values stored in a tenant-scoped database collection.
* **API** -- values fetched from an external HTTP endpoint.
```yaml expandable=true theme={null}
LOOKUP_TABLES:
airport_codes:
source: inline
values: [JFK, LAX, ORD, SFO, MIA, ATL, DFW, SEA]
case_sensitive: false
fuzzy_match: true
fuzzy_threshold: 0.85
hotel_chains:
source: collection
table_name: "hotel_chains"
field: "name"
case_sensitive: false
fuzzy_match: true
fuzzy_threshold: 0.8
currency_codes:
source: api
endpoint: "https://api.reference.com/v1/currencies"
field: "code"
timeout_ms: 5000
case_sensitive: true
fuzzy_match: false
```
### Inline lookup tables
Inline tables define their values directly in the ABL file. Use them for small, stable reference sets.
#### Syntax
```yaml theme={null}
LOOKUP_TABLES:
transfer_types:
source: inline
values: [domestic, international]
case_sensitive: false
fuzzy_match: false
```
#### When to use inline tables
* The value set is small (fewer than \~100 entries).
* The values rarely change.
* No external dependency is acceptable.
### Collection lookup tables
Collection tables read values from a tenant-scoped database collection. The platform resolves the `table_name` to the correct storage location based on the current tenant.
#### Syntax
```yaml theme={null}
LOOKUP_TABLES:
product_catalog:
source: collection
table_name: "products"
field: "product_name"
case_sensitive: false
fuzzy_match: true
fuzzy_threshold: 0.85
```
#### When to use collection tables
* The value set is large or changes frequently.
* Values are managed through an admin interface or data pipeline.
* Different tenants have different valid values.
### API lookup tables
API tables fetch values from an external HTTP endpoint at runtime. The response is expected to contain an array of objects; the `field` property specifies which object field to match against.
#### Syntax
```yaml expandable=true theme={null}
LOOKUP_TABLES:
exchange_rates:
source: api
endpoint: "https://api.rates.example.com/v1/currencies"
field: "currency_code"
timeout_ms: 3000
case_sensitive: true
fuzzy_match: false
```
#### When to use API tables
* Values come from a third-party system that maintains its own data.
* Real-time accuracy is important (for example, live exchange rates, inventory).
* The dataset is too large to store locally.
### Lookup table properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `source` | `string` | Yes | `inline` | Data source type: `inline`, `collection`, or `api`. Legacy aliases: `mongodb` → `collection`, `http` → `api`. |
| `values` | `string[]` | No | -- | Static values (required for `source: inline`). |
| `map` | `Record` | No | -- | Inline key → value mapping (an alternative to `values`; keys become the valid values). |
| `table_name` | `string` | No | -- | Logical table name (required for `source: collection`). Alias: `collection`. |
| `endpoint` | `string` | No | -- | HTTP endpoint URL (required for `source: api`). |
| `field` | `string` | No | -- | Field within the data source to match against. |
| `headers` | `Record` | No | -- | Custom HTTP headers for `source: api` (for example, an `Authorization` header). |
| `timeout_ms` | `number` | No | `5000` | Request timeout in milliseconds (for `source: api`). |
| `case_sensitive` | `boolean` | No | `false` | Whether matching is case-sensitive. |
| `fuzzy_match` | `boolean` | No | `false` | Whether fuzzy (approximate) matching is enabled. |
| `fuzzy_threshold` | `number` | No | `0.85` | Minimum similarity score (0.0--1.0) for fuzzy matches to be accepted. |
Table names must match `^[a-z_][a-z0-9_]*$` and field names `^[a-zA-Z_][a-zA-Z0-9_.]*$`. A small inline map table can also be written in shorthand: `my_table: { key1: value1, key2: value2 }`.
### Fuzzy matching
When `fuzzy_match: true`, the runtime uses string similarity algorithms to find the closest match when an exact match is not found. This handles typos, abbreviations, and minor variations.
#### Similarity threshold
The `fuzzy_threshold` controls how similar a user's input must be to a valid value for the match to be accepted:
| Threshold | Behavior |
| - | - |
| `0.95` | Very strict. Only near-exact matches accepted. |
| `0.85` | Default. Tolerates minor typos and abbreviations. |
| `0.75` | Lenient. Accepts more variation but increases false positive risk. |
| `0.50` | Very lenient. Useful for short strings where small edits are significant. |
#### Example: fuzzy matching for airport codes
```yaml theme={null}
LOOKUP_TABLES:
airports:
source: inline
values: [JFK, LAX, ORD, SFO, MIA, ATL, DFW, SEA]
case_sensitive: false
fuzzy_match: true
fuzzy_threshold: 0.8
```
With this configuration:
* `"jfk"` matches `JFK` (case-insensitive exact match).
* `"SFP"` matches `SFO` (fuzzy match, 1 character difference).
* `"XYZ"` doesn't match any value (below threshold).
### Field mapping
The `field` property specifies which field in the data source to match with. For collection and API sources, the data is typically an array of objects. The `field` tells the runtime which property to compare:
```yaml theme={null}
# Given API response: [{"code": "USD", "name": "US Dollar"}, {"code": "EUR", "name": "Euro"}]
LOOKUP_TABLES:
currencies:
source: api
endpoint: "https://api.reference.com/v1/currencies"
field: "code"
case_sensitive: true
fuzzy_match: false
```
User input is matched with the `code` field of each object in the response.
### Using lookup tables
A GATHER field references a lookup table through its `semantics.lookup` metadata, which names the
table used to validate and normalize the collected value (applying the table's case-sensitivity,
fuzzy matching, and threshold):
```yaml theme={null}
GATHER:
destination_airport:
prompt: "What's your destination airport code?"
type: string
required: true
semantics:
lookup: airports
```
Tool parameters can source a value from a lookup table via `SOURCE: lookup()` in a
`.tools.abl` parameter block (see [Tools](/agent-platform/abl-reference/tools)); this resolves to a
`{{lookup::}}` value at runtime.
### Timeout and error handling
For API-sourced tables, specify a `timeout_ms` to control how long the runtime waits for the external service:
```yaml theme={null}
LOOKUP_TABLES:
products:
source: api
endpoint: "https://catalog.internal/api/v1/products"
field: "sku"
timeout_ms: 3000
case_sensitive: true
fuzzy_match: false
```
If the API call times out or returns an error, the runtime:
1. Logs a warning.
2. Falls back to accepting the user's input without validation.
3. Emits a trace event indicating the lookup failure.
***
## Attachments
Attachments define file and media upload fields that the agent collects from the user. The `ATTACHMENTS:` block declares named attachment fields with their content category, processing options, and validation constraints. Attachments work alongside [GATHER](/agent-platform/abl-reference/gather) fields to collect structured data through conversation.
### Overview
ABL supports four attachment categories, each with specialized processing capabilities:
| Category | Use cases | Processing features |
| - | - | - |
| `document` | PDFs, Word docs, spreadsheets, text files | OCR, text extraction |
| `image` | Photos, screenshots, scanned documents | OCR, image analysis |
| `audio` | Voice recordings, audio messages | Transcription |
| `video` | Screen recordings, instructional clips | Keyframe extraction, transcription |
```yaml expandable=true theme={null}
ATTACHMENTS:
id_document:
prompt: "Please upload a photo of your government-issued ID."
category: image
required: true
max_file_size_mb: 10
allowed_types: [image/jpeg, image/png, image/webp]
ocr_enabled: true
supporting_docs:
prompt: "Upload any supporting documents (invoices, contracts, etc.)."
category: document
required: false
max_file_size_mb: 25
allowed_types: [application/pdf, image/jpeg, image/png]
ocr_enabled: true
```
### Attachment field properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | `string` | Yes | -- | Unique identifier for the attachment field (the YAML key). |
| `prompt` | `string` | Yes | -- | Message displayed to the user when requesting the upload. |
| `category` | `string` | No | `document` | Content category: `document`, `image`, `audio`, or `video`. |
| `required` | `boolean` | No | `true` | Whether the attachment is mandatory. |
| `max_file_size_mb` | `number` | No | -- | Maximum file size in megabytes. Alias: `max_size_mb`. |
| `allowed_types` | `string[]` | No | -- | Allowed MIME types. If omitted, all types for the category are accepted. |
| `ocr_enabled` | `boolean` | No | `false` | Enable OCR text extraction for `document` and `image` categories. |
| `transcription_enabled` | `boolean` | No | `false` | Enable audio transcription for `audio` and `video` categories. Alias: `transcription`. |
| `key_frame_extraction` | `boolean` | No | `false` | Enable keyframe extraction for `video` category. |
The DSL keyword for the MIME allowlist is **`allowed_types`** (it populates the field's allowed MIME types). `allowed_mime_types` is not recognized by the `.agent.abl` parser.
### Document attachments
Document attachments handle file uploads such as PDFs, Word documents, spreadsheets, and plain text files. When OCR is enabled, the runtime extracts text content from the uploaded document and makes it available in the session context.
```yaml theme={null}
ATTACHMENTS:
bank_statement:
prompt: "Please upload your most recent bank statement."
category: document
required: true
max_file_size_mb: 25
allowed_types: [application/pdf]
ocr_enabled: true
```
#### Common document MIME types
| MIME type | Description |
| - | - |
| `application/pdf` | PDF documents |
| `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | Word (.docx) |
| `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | Excel (.xlsx) |
| `text/plain` | Plain text |
| `text/csv` | CSV files |
### Image attachments
Image attachments handle photo and screenshot uploads. OCR extracts text from images (useful for scanned documents, receipts, and ID cards).
```yaml theme={null}
ATTACHMENTS:
receipt_photo:
prompt: "Take a photo of your receipt or upload an image."
category: image
required: true
max_file_size_mb: 10
allowed_types: [image/jpeg, image/png, image/webp, image/heic]
ocr_enabled: true
```
#### Common image MIME types
| MIME type | Description |
| - | - |
| `image/jpeg` | JPEG images |
| `image/png` | PNG images |
| `image/webp` | WebP images |
| `image/heic` | HEIC images (iOS) |
| `image/gif` | GIF images |
| `image/tiff` | TIFF images |
### Audio attachments
Audio attachments handle voice recordings and audio messages. When transcription is enabled, the runtime converts the audio to text.
```yaml theme={null}
ATTACHMENTS:
voice_message:
prompt: "Record or upload a voice message describing the issue."
category: audio
required: false
max_file_size_mb: 50
allowed_types: [audio/mpeg, audio/wav, audio/ogg, audio/webm]
transcription_enabled: true
```
#### Common audio MIME types
| MIME type | Description |
| - | - |
| `audio/mpeg` | MP3 files |
| `audio/wav` | WAV files |
| `audio/ogg` | OGG audio |
| `audio/webm` | WebM audio |
| `audio/mp4` | AAC/M4A files |
### Video attachments
Video attachments handle video file uploads. Two processing features are available:
* **Transcription** extracts speech from the video's audio track.
* **Keyframe extraction** captures representative frames from the video for visual analysis.
```yaml expandable=true theme={null}
ATTACHMENTS:
screen_recording:
prompt: "Upload a screen recording showing the issue."
category: video
required: false
max_file_size_mb: 100
allowed_types: [video/mp4, video/webm, video/quicktime]
transcription_enabled: true
key_frame_extraction: true
```
#### Common video MIME types
| MIME type | Description |
| - | - |
| `video/mp4` | MP4 video |
| `video/webm` | WebM video |
| `video/quicktime` | QuickTime (MOV) |
### OCR processing
When `ocr_enabled: true`, the runtime processes uploaded documents and images through an OCR pipeline that:
1. Detects text regions in the file.
2. Extracts text content.
3. Stores the extracted text in the session context under the attachment field name.
The extracted text is available in expressions and template strings:
```yaml theme={null}
# After OCR, the text is available as the attachment field name
SET:
id_text = id_document.extracted_text
```
### Transcription processing
When `transcription_enabled: true`, the runtime processes audio and video files through a transcription pipeline:
1. Extracts the audio track (for video files).
2. Runs speech-to-text transcription.
3. Stores the transcript in the session context.
```yaml theme={null}
SET:
message_text = voice_message.transcript
```
### Keyframe extraction
When `key_frame_extraction: true`, the runtime extracts representative frames from video files:
1. Analyzes the video for scene changes.
2. Captures keyframes at scene boundaries.
3. Stores the keyframes as an array of image references.
```yaml theme={null}
SET:
frames = screen_recording.keyframes
```
### File size and MIME type validation
The runtime validates uploaded files against the declared constraints before processing:
* If the file exceeds `max_file_size_mb`, the upload is rejected with an error message.
* If the file's MIME type is not in `allowed_types`, the upload is rejected.
* If `allowed_types` is omitted, all standard MIME types for the category are accepted.
### Complete attachment example
```yaml expandable=true theme={null}
ATTACHMENTS:
government_id:
prompt: "Please upload a clear photo of your government-issued ID (passport, driver's license, or national ID card)."
category: image
required: true
max_file_size_mb: 10
allowed_types: [image/jpeg, image/png, image/webp]
ocr_enabled: true
proof_of_address:
prompt: "Upload a utility bill or bank statement from the last 3 months as proof of address."
category: document
required: true
max_file_size_mb: 25
allowed_types: [application/pdf, image/jpeg, image/png]
ocr_enabled: true
voice_authorization:
prompt: "Record a voice message stating: 'I authorize this wire transfer of {{amount}} {{currency}} to {{beneficiary_name}}.'"
category: audio
required: false
max_file_size_mb: 10
allowed_types: [audio/mpeg, audio/wav, audio/webm]
transcription_enabled: true
```
***
## Destinations
The `DESTINATIONS:` block declares named **outbound HTTP targets** for delivering data or
notifications to external systems. Each destination has a name (the entry key), a `url`, and
optional method/auth/headers.
```yaml expandable=true theme={null}
DESTINATIONS:
doc_processor:
url: "https://api.docprocessor.com/ingest"
method: POST
auth: bearer_token
headers:
X-Source: "abl-agent"
Authorization: "Bearer {{config.PARTNER_API_KEY}}"
archive:
url: "https://archive.example.com/upload"
method: PUT
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | `string` | Yes | -- | Destination identifier (the entry key). |
| `url` | `string` | Yes | -- | Outbound URL. Validated for SSRF at compile time. |
| `method` | `string` | No | -- | HTTP method (`GET`, `POST`, `PUT`, …), upper-cased by the parser. |
| `auth` | `string` | No | -- | Auth strategy reference (resolved from project config — no secrets in ABL). |
| `headers` | `Record` | No | -- | Custom HTTP headers. Supports `{{config.X}}` placeholders. |
***
**Related articles:**
* [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) -- type coercion in expressions and built-in type-checking functions
* [Memory and Constraints](/agent-platform/abl-reference/memory-and-constraints) -- TYPE declarations in session and persistent variables, attachment data in session
* [GATHER](/agent-platform/abl-reference/gather) -- gather field types and lookup-based validation
* [NLU](/agent-platform/abl-reference/nlu) -- entity extraction that may reference lookup tables
# Expressions and functions
Source: https://koreai.mintlify.app/agent-platform/abl/reference/expressions-and-functions
This page documents ABL's expression language: operators, variable paths, `{{}}` template-string interpolation, the 43 built-in functions, and type coercion rules. Expressions are used in conditions (`WHEN`, `CHECK`, constraint rules), value assignments (`SET`), template interpolation (`{{}}`), and function calls. For rich content output formats (voice, cards, carousels, interactive actions, templates), see [Rich content](/agent-platform/abl-reference/rich-content).
***
## Expression syntax
### Comparison operators
| Operator | Syntax | Description |
| - | - | - |
| `==` | `a == b` | Equal to. |
| `!=` | `a != b` | Not equal to. |
| `>` | `a > b` | Greater than. |
| `<` | `a < b` | Less than. |
| `>=` | `a >= b` | Greater than or equal to. |
| `<=` | `a <= b` | Less than or equal to. |
| `in` | `a IN [x,y,z]` | Value is in the list. |
| `not_in` | `a NOT IN [x]` | Value is not in the list. |
| `matches` | `a matches r` | Value matches a regular expression. |
| `contains` | `a contains b` | String contains substring, or array contains element. |
| `startsWith` | `a startsWith b` | String starts with a prefix. |
| `endsWith` | `a endsWith b` | String ends with a suffix. |
### Logical operators
| Operator | Syntax | Description |
| - | - | - |
| `AND` | `a AND b` | Both conditions must be true. |
| `OR` | `a OR b` | At least one must be true. |
| `NOT` | `NOT a` | Negates the condition. |
Write logical operators in uppercase (`AND`, `OR`, `NOT`). `!` is also accepted as a prefix negation.
`contains`, `matches`, `startsWith`, and `endsWith` are infix **operators** (`a contains b`), not callable functions — a condition that calls one as a bare function (`CONTAINS(ani_national, "1234567890")`) parses but has no matching overload, so it can never evaluate true; the compiler warns `CEL_CONDITION_UNRESOLVED_CALL`. Use the infix form, or the `abl.*` CEL method form (`x.contains("y")`).
### Unary operators
| Operator | Syntax | Description |
| - | - | - |
| `NOT` / `!` | `NOT condition` / `!condition` | Negates a condition. |
| `IS SET` | `var IS SET` | True if the variable is not null/undefined (empty string counts as set). |
| `IS NOT SET` | `var IS NOT SET` | True if the variable is null or undefined. |
| `is_number` | `var is_number` | True if the variable is a number. |
### Operator precedence
1. Parentheses `()`
2. Unary operators (`NOT`, `IS SET`, `IS NOT SET`)
3. Comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `matches`)
4. `AND`
5. `OR`
Use parentheses to override default precedence:
```yaml theme={null}
WHEN: (status == "active" OR status == "pending") AND amount > 0
```
### Quoting a condition
A `WHEN` value — in `HANDOFF`, `ON_RETURN WHEN`, `ESCALATE` triggers, `COMPLETE`, and hook `WHEN`/`RESPOND` branches — is unwrapped from its quotes and evaluated as a condition whenever it contains a condition operator: `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, `IS [NOT] SET`, `contains`, `matches`, `startsWith`, `endsWith`, `in`, or a function call. So `WHEN: "dispute == true"` and `WHEN: dispute == true` are the same condition. A quoted value with none of these — a natural-language phrase such as `WHEN: "task done"` — stays a quoted literal string, which most deterministic evaluators treat as always-truthy.
Write a genuinely deterministic condition either unquoted or quoted-with-an-operator; reserve a bare quoted phrase for constructs that intentionally accept free text (a prompt-only/semantic condition). A deterministic routing or escalation entry's `when` must be an operator expression regardless of quoting — see [Deterministic-only routing](/agent-platform/abl-reference/multi-agent-and-supervisor#conditional-routing-entries).
## Variable paths and dot notation
Reference variables using dot notation to access nested values.
```yaml theme={null}
user.name # Access nested property
user.addresses[0].city # Array index access
acctResult.status # Tool result field
```
### Path resolution rules
* The evaluator looks up the full path in the session context.
* The Platform resolves each segment left to right: `user.address.city` resolves `user`, then `address` on the result, then `city`.
* If any segment resolves to `null` or `undefined`, the entire path resolves to `undefined`.
* Array access uses bracket notation: `items[0]`, `items[2].name`.
## Template strings
Template strings use `{{}}` syntax for variable interpolation within `RESPOND`, `summary`, and other string properties -- including inside a [`TEMPLATES:` block](/agent-platform/abl-reference/rich-content#templates) body:
```yaml theme={null}
RESPOND: "Hello, {{customer_name}}! Your balance is {{available_balance}}."
```
### Conditional sections
Templates support conditional rendering with `{{#if}}...{{/if}}`:
```yaml theme={null}
RESPOND: |
Transfer complete.
{{#if fx_rate}}Exchange rate: {{fx_rate}}{{/if}}
Estimated arrival: {{estimated_arrival}}.
```
### Function calls in templates
You can call built-in functions within template strings:
```yaml theme={null}
SET:
formatted_balance = FORMAT_CURRENCY(available_balance, "USD")
RESPOND: "Your balance is {{formatted_balance}}."
```
## Built-in function reference
ABL provides 36 built-in functions organized into seven categories (plus the CEL-only content-analysis helpers described below). All functions are called with `FUNCTION_NAME(arg1, arg2, ...)` syntax. Function names are **uppercase**.
### Math functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `ADD` | `ADD(a, b) -> number` | Add two numbers. | `ADD(2, 3)` returns `5` |
| `SUB` | `SUB(a, b) -> number` | Subtract b from a. | `SUB(10, 3)` returns `7` |
| `MUL` | `MUL(a, b) -> number` | Multiply two numbers. | `MUL(4, 5)` returns `20` |
| `DIV` | `DIV(a, b) -> number` | Divide a by b. Returns `null` for division by zero. | `DIV(10, 2)` returns `5` |
| `ROUND` | `ROUND(n, decimals?) -> number` | Round to specified decimal places. Default: 0. | `ROUND(3.14159, 2)` returns `3.14` |
| `ABS` | `ABS(n) -> number` | Absolute value. | `ABS(-5)` returns `5` |
| `MIN` | `MIN(a, b) -> number` | Return the smaller of two numbers. | `MIN(3, 7)` returns `3` |
| `MAX` | `MAX(a, b) -> number` | Return the larger of two numbers. | `MAX(3, 7)` returns `7` |
Math functions coerce string arguments to numbers automatically: `ADD("2", "3")` returns `5`.
### String functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `UPPER` | `UPPER(s) -> string` | Convert to uppercase. | `UPPER("hello")` returns `"HELLO"` |
| `LOWER` | `LOWER(s) -> string` | Convert to lowercase. | `LOWER("HELLO")` returns `"hello"` |
| `TRIM` | `TRIM(s) -> string` | Strip leading/trailing whitespace. | `TRIM(" hi ")` returns `"hi"` |
| `SUBSTRING` | `SUBSTRING(s, start, end?) -> string` | Extract a substring. | `SUBSTRING("hello", 0, 3)` returns `"hel"` |
| `REPLACE` | `REPLACE(s, find, repl) -> string` | Replace all occurrences. | `REPLACE("a,b,c", ",", "-")` returns `"a-b-c"` |
| `SPLIT` | `SPLIT(s, delimiter) -> string[]` | Split string into array. | `SPLIT("a,b,c", ",")` returns `["a","b","c"]` |
| `JOIN` | `JOIN(arr, delimiter) -> string` | Join array into string. | `JOIN(["a","b"], ", ")` returns `"a, b"` |
| `PAD_START` | `PAD_START(s, length, char?) -> string` | Left-pad to specified length. | `PAD_START("42", 6, "0")` returns `"000042"` |
| `PAD_END` | `PAD_END(s, length, char?) -> string` | Right-pad to specified length. | `PAD_END("42", 6, "0")` returns `"420000"` |
| `REPEAT` | `REPEAT(s, count) -> string` | Repeat string N times. | `REPEAT("*", 4)` returns `"****"` |
| `REGEX_REPLACE` | `REGEX_REPLACE(text, pattern, replacement) -> string` | RE2-backed replace-all; `$1`-`$9` in `replacement` insert capture groups. Matching is single-pass, so `^`/`\b` anchor against the *original* string, not partial replacement output. Returns `text` unchanged on an invalid pattern — never throws. | `REGEX_REPLACE("00123", "^0", "")` returns `"0123"` |
| `REGEX_EXTRACT` | `REGEX_EXTRACT(text, pattern, group?) -> string \| null` | First match, or capture group `group` when given. Returns `null` when nothing matches or the pattern is invalid — never throws. | `REGEX_EXTRACT("order-4821", "\\d+")` returns `"4821"` |
The string functions gracefully handle `null` and `undefined` and return an empty string: `UPPER(null)` returns `""`.
To prevent memory issues, the limit on string output is 100,000 characters.
### Identity-matching functions
These compare what a caller actually said against a stored value — they are deterministic similarity scoring, not a semantic/model judgment, built for voice identity verification (see [Write authority](/agent-platform/abl-reference/memory-and-constraints#write-authority) for a `READONLY` field that stores the result).
| Function | Signature | Description |
| - | - | - |
| `MATCH_SCORE` | `MATCH_SCORE(said, stored, kind?) -> number` | Similarity 0-100 between `said` and `stored`. Returns `-1` when there's nothing to judge — a missing/empty value, a PII vault token, or an input over 1,024 characters. `-1` is below every threshold, so `MATCH_SCORE(...) >= 70` fails closed. |
| `MATCHES` | `MATCHES(said, stored, kind?) -> boolean` | Pass/fail form of `MATCH_SCORE` on the platform's own match bands (below). **Not a regex test** — the infix `a MATCHES "pattern"` operator (see [Comparison operators](#comparison-operators)) is the regex form. |
| `MATCH_RESULT` | `MATCH_RESULT(said, stored, kind?) -> string` | `"match"`, `"retry"` (a tight near-miss only), `"mismatch"`, or `"missing"`. |
`kind` is optional — omitted, it defaults to `text` (unchanged behavior, no warning) — and otherwise one of:
| `kind` | Meaning |
| - | - |
| `name` / `person_name` | A personal name, scored part-by-part on how it sounds. |
| `email` | An email address, read from spoken forms ("at the rate", "dot", spelled letters). |
| `address` | A street address, read part by part in any order. |
| `phone` / `digits` | Digits only, with spoken-digit and trunk/country-code handling (`phone`), or an exact digit-for-digit identifier like a member number (`digits` — no national-number truncation). |
| `text` | Plain text similarity (the default). |
| `auto` | Detects the kind from the **stored** value's shape: an `@` with a dotted domain → `email`; mostly digits with 7-10 digits or a leading `+` → `phone`; digits only, under 7 or 11+ with no `+` → `digits`; a house-number-plus-street-type pattern → `address`; 2-4 capitalized words → `person_name`; otherwise `text`. An explicit kind (including `"text"`) is never auto-detected — `"auto"` only applies when you pass it. |
| `exact` | Turns off every voice allowance: matches only after Unicode normalization, trimming, case-folding, and collapsing spaces/trailing punctuation — "Steven" does not match "Stephen". |
Platform-owned match bands (authors never pick a threshold):
| Kind | `match` | `retry` | `mismatch` |
| - | - | - | - |
| `phone`, `digits` | every digit matches (100) | exactly one digit slip, or two neighbouring digits swapped (50) | anything else |
| `email`, `name`/`person_name`, `address`, `text` | score ≥ 70 | score 50-69 (a tight near-miss only) | score \< 50 |
A `-1` score (nothing to judge) always reports `"missing"`.
The guiding principle: values that *sound* the same cannot be told apart by a caller (speech-to-text just picks a spelling), so they score as a `"match"` — only values that sound genuinely different can fail. For example `email` compares the domain by sound but matches its ending exactly (`kore.ai` is not `kore.com`); `person_name` matches common respellings (`Jon`/`John`, `Steven`/`Stephen`) and scores a near-same name (`Ahmed`/`Ahmad`) as `"retry"`, never a straight mismatch, while a genuinely different name (`Mark`/`Mary`) is a `"mismatch"`; `address` reads house/unit letters and matches street words that sound alike. The matcher reads the value itself, not the sentence around it — filler words ("no", "sorry", "I mean") are not stripped, so an answer that contains them alongside the correct value still fails to match as written.
`said` must be the extracted field (for example `supplied_email`), not the raw caller utterance. An extracted field is written by the model and may already hold the stored value (for example, after a lookup tool returned the profile) — a match on the field alone does not prove the caller actually said it. A rule that grants access on a match should also confirm the caller's own words contained the value.
The **function** `MATCHES(said, stored[, kind])` and the **infix operator** `a MATCHES "pattern"` are unrelated — do not mix up their second argument. Writing `MATCHES(supplied, "^gold$")` (the function form with a regex-shaped literal) is almost certainly meant to be the infix form `supplied MATCHES "^gold$"`, and the compiler warns `MATCHES_FUNCTION_REGEX_LITERAL`. Conversely, writing `tier MATCHES "gold"` (the infix form with a plain literal) is true whenever `"gold"` appears anywhere in `tier` as a substring — not a fuzzy spoken-answer comparison — and the compiler warns `MATCHES_INFIX_LITERAL_AMBIGUOUS` since this is easy to mean the other way.
### Formatting functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `MASK` | `MASK(s, pattern, char?) -> string` | Mask a string. See Mask Patterns below. | `MASK("4111111111111111", "last4")` returns `"************1111"` |
| `FORMAT_CURRENCY` | `FORMAT_CURRENCY(n, currency, locale?) -> string` | Format a number as currency. | `FORMAT_CURRENCY(1234.5, "USD")` returns `"$1,234.50"` |
| `FORMAT_DATE` | `FORMAT_DATE(d, format, tz?) -> string` | Format a date string. | `FORMAT_DATE("2024-03-15T10:30:00Z", "YYYY-MM-DD")` returns `"2024-03-15"` |
| `ORDINAL` | `ORDINAL(n) -> string` | Convert number to ordinal string. | `ORDINAL(1)` returns `"1st"`, `ORDINAL(22)` returns `"22nd"` |
The mask patterns are explained below.
| Pattern | Behavior | Example result |
| - | - | - |
| `last4` | Mask all characters except the last 4. | `"************1111"` |
| `first4` | Mask all characters except the first 4. | `"4111************"` |
| `N*N` | Show N chars at start, mask middle, show N at end. | `MASK(s, "4*4")` shows 4+4 |
The default mask character is `*`. Pass a third argument to use a different character: `MASK(ssn, "last4", "x")`.
The date format uses the following notations:
| Token | Description | Example |
| - | - | - |
| `YYYY` | Four-digit year | `2024` |
| `MM` | Two-digit month | `03` |
| `DD` | Two-digit day | `15` |
| `HH` | Two-digit hour (24h) | `10` |
| `mm` | Two-digit minute | `30` |
| `ss` | Two-digit second | `00` |
Example: `FORMAT_DATE("2024-03-15T10:30:00Z", "YYYY-MM-DD HH:mm")` returns `"2024-03-15 10:30"`.
`FORMAT_DATE` formats in **UTC by default** — a date-time string without an explicit offset is interpreted as UTC, so the output is deterministic regardless of where the runtime executes. Pass an optional IANA timezone name as the third argument to format in that zone instead:
```
FORMAT_DATE("2024-03-15T10:30:00Z", "YYYY-MM-DD HH:mm", "America/New_York") # "2024-03-15 06:30"
```
An unrecognized timezone falls back to UTC.
### Type checking and coercion functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `IS_ARRAY` | `IS_ARRAY(x) -> boolean` | True if the value is an array. | `IS_ARRAY([1,2])` returns `true` |
| `IS_NUMBER` | `IS_NUMBER(x) -> boolean` | True if the value is a number (not NaN). | `IS_NUMBER(42)` returns `true` |
| `IS_STRING` | `IS_STRING(x) -> boolean` | True if the value is a string. | `IS_STRING("hi")` returns `true` |
| `TO_NUMBER` | `TO_NUMBER(x) -> number` | null | Convert to number. Returns `null` if conversion fails. `TO_NUMBER("42")` returns `42` |
| `TO_STRING` | `TO_STRING(x) -> string` | Convert to string. Returns `""` for null/undefined. | `TO_STRING(42)` returns `"42"` |
### Array functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `LENGTH` | `LENGTH(x) -> number` | Array length or string length. Returns 0 for other types. | `LENGTH([1,2,3])` returns `3` |
| `ARRAY_FIND` | `ARRAY_FIND(arr, field, value) -> object` | null | Find first element where `field == value`. `ARRAY_FIND(users, "id", 42)` returns the matching object |
| `ARRAY_FIND_INDEX` | `ARRAY_FIND_INDEX(arr, field, value) -> number` | Find index of first match. Returns -1 if not found. | `ARRAY_FIND_INDEX(items, "type", "b")` returns the index |
### Object functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `OBJECT_KEYS` | `OBJECT_KEYS(obj) -> string[]` | Return array of object keys. | `OBJECT_KEYS({a:1, b:2})` returns `["a","b"]` |
| `OBJECT_VALUES` | `OBJECT_VALUES(obj) -> any[]` | Return array of object values. | `OBJECT_VALUES({a:1, b:2})` returns `[1, 2]` |
| `OBJECT_MERGE` | `OBJECT_MERGE(obj1, obj2, ...) -> object` | Merge objects. Right-side values win on conflict. | `OBJECT_MERGE({a:1}, {b:2})` returns `{a:1, b:2}` |
`OBJECT_KEYS` and `OBJECT_VALUES` return an empty array for non-object inputs (null, arrays, strings).
`OBJECT_MERGE` accepts any number of arguments and skips non-object values.
### Utility functions
| Function | Signature | Description | Example |
| - | - | - | - |
| `COALESCE` | `COALESCE(a, b, ...) -> any` | Return the first non-null, non-undefined value. Returns `null` if all arguments are null. | `COALESCE(null, undefined, "hello")` returns `"hello"` |
| `FIRST_NON_EMPTY` | `FIRST_NON_EMPTY(a, b, ...) -> any` | Return the first value that is not null, undefined, `""`, or blank. Unlike `COALESCE`, it skips an empty/blank string. | `FIRST_NON_EMPTY("", null, "hello")` returns `"hello"` |
| `IF` | `IF(cond, a, b) -> any` | Eager conditional — both `a` and `b` are evaluated regardless of `cond`. Use the ternary `cond ? a : b` for lazy evaluation. | `IF(amount > 100, "high", "low")` |
| `NOW` | `NOW() -> string` | Return the current timestamp as an ISO 8601 string. | `NOW()` returns `"2024-03-15T10:30:00.000Z"` |
| `UNIQUE_ID` | `UNIQUE_ID(length?) -> string` | Generate a random alphanumeric string. Default length: 6. | `UNIQUE_ID(12)` returns `"aB3kM9pQ2xLw"` |
`COALESCE` considers `0` and `false` as valid (non-null) values: `COALESCE(0, "fallback")` returns `0`.
`UNIQUE_ID` generates a random alphanumeric string suitable for reference numbers. It is **not** cryptographically secure — do not use it for tokens or secrets.
### Content-analysis functions (CEL guardrails)
CEL-based [guardrail](/agent-platform/abl-reference/guardrails) checks have access to an additional
`abl.*` namespace of content-analysis helpers:
| Function | Description |
| - | - |
| `abl.contains_pii(text)` | True if the text contains PII. |
| `abl.detect_pii(text)` | Structured PII detection result. |
| `abl.redact_pii(text)` | Returns the text with PII redacted. |
| `abl.matches_pattern(text, pattern)` | Regex match (RE2-backed). |
| `abl.not_matches_pattern(text, pattern)` | Negation of `matches_pattern`. |
| `abl.word_count(text)` | Word count. |
| `abl.sentence_count(text)` | Sentence count. |
| `abl.contains_url(text)` | True if the text contains a URL. |
| `abl.contains_email(text)` | True if the text contains an email address. |
| `abl.contains_code(text)` | True if the text contains a fenced code block. |
| `abl.pii_equal(left, right)` | True if two **PII-vault tokens** hold the same underlying value. See below. |
In CEL expressions the core functions above are also available under the `abl.` namespace with lowercase names (for example, `abl.upper(x)`), and CEL-native operators (`&&`, `||`, `!`, `%`, `in`, ternary `? :`) and macros (`size()`, `has()`) can be used.
`abl.pii_equal(left, right)` compares two values **without ever exposing the underlying plaintext to the expression** — both `left` and `right` must be live PII-vault tokens (the opaque form a PII-classified value takes once captured, for example, via a `sensitive: true` GATHER field), and the comparison happens inside the vault itself. Values are canonicalized before comparing (email lowercased/trimmed, phone/SSN/credit-card digits-only, etc.), so `abl.pii_equal(session.data.values.email, caller_provided_email)` matches `Member@Example.com` against `member@example.com`.
```yaml theme={null}
CHECK: abl.pii_equal(session.data.values.email, caller_provided_email)
ON_FAIL: identity_mismatch
```
If either operand isn't a live vault token for the same PII type — missing, expired, or a plain string — `abl.pii_equal` **throws** rather than returning `false`. This is deliberate: silently returning `false` on a fault would make `!abl.pii_equal(x, y)` evaluate `true` on a comparison that couldn't actually be performed. Guard against this with `IS SET` checks on both operands before comparing, or route the resulting evaluation error through your usual error handling.
### Nested function calls
Functions can be nested as arguments to other functions:
```yaml theme={null}
SET:
result = ADD(MUL(2, 3), SUB(10, 4)) # Returns 12
name = UPPER(TRIM(" hello ")) # Returns "HELLO"
fallback = COALESCE(user.lastName, "Guest")
```
Nesting is limited to a maximum depth of 32 to prevent stack overflow.
## Using expressions in ABL
### In conditions (WHEN, CHECK, constraint rules)
```yaml theme={null}
WHEN: user.age >= 18 AND user.verified == true
CHECK: amount <= available_balance
- REQUIRE sanctions_clear == true
```
### In SET assignments
```yaml theme={null}
SET:
balance_formatted = FORMAT_CURRENCY(available_balance, "USD")
wire_reference = UNIQUE_ID(12)
greeting = COALESCE(user.name, "valued customer")
```
`recent_input` is a built-in available in `SET` contexts: the caller's last 3 utterances, joined. `input_transcript` is a built-in, read-only `SET` value on realtime voice channels — the provider's transcript of the current caller turn (empty when there is none). An authored session variable with either name takes precedence over the built-in.
### In template interpolation
```yaml theme={null}
RESPOND: "Hello, {{customer_name}}! Balance: {{FORMAT_CURRENCY(available_balance, 'USD')}}."
```
### In TRANSFORM pipelines
```yaml theme={null}
TRANSFORM:
SOURCE: search_results
AS: item
INTO: filtered_results
FILTER: item.price <= budget AND item.available == true
MAP:
name: item.hotel_name
price: FORMAT_CURRENCY(item.price, "USD")
SORT_BY: price ASC
LIMIT: 5
```
## Type coercion rules
The evaluator applies these coercion rules during expression evaluation:
| Context | Rule |
| - | - |
| Equality (`==`) | null/undefined == null/undefined is true. Strings compared case-sensitively. Numbers parsed from strings. |
| Inequality (`!=`) | If either side is undefined/null, returns true. |
| Numeric (`>`, `<`, etc.) | Strings parsed to numbers. Booleans become 0/1. Arrays become length. Undefined becomes 0. |
| Truthiness (bare variable) | `false`, `0`, `""`, `"false"`, `null`, `undefined`, empty arrays, empty objects are falsy. Everything else is truthy. |
| IS SET / IS NOT SET | Checks for null/undefined only. Empty string IS SET. |
| Arithmetic (`+ - * / %`) | Works between a decimal session number and an integer literal, in either order, producing a decimal result (for example `turn_count == followup_offer_turn + 1`). Division or modulus by zero is an error — the condition does not hold — never `Infinity`. |
***
**Related articles:**
* [Rich content](/agent-platform/abl-reference/rich-content) -- voice configuration, format-specific output, carousels, interactive actions, and templates
* [Memory and constraints](/agent-platform/abl-reference/memory-and-constraints) -- condition expressions in constraint rules, variable paths and session context
* [Multi-Agent and supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) -- condition syntax for WHEN clauses in routing/escalation
* [Data Types and utilities](/agent-platform/abl-reference/data-types-and-utilities) -- types used in function signatures, lookup validation functions
# FLOW (structured execution steps)
Source: https://koreai.mintlify.app/agent-platform/abl/reference/flow
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
```yaml expandable=true theme={null}
FLOW:
entry_point: start
steps:
- start
- collect_info
- process
- complete
start:
REASONING: false
SET:
status = "pending"
RESPOND: "Welcome! Let me help you get started."
THEN: collect_info
collect_info:
REASONING: false
GATHER:
- name: required
prompt: "What is your name?"
THEN: process
process:
REASONING: false
CALL: process_request
WITH:
name: name
AS: result
RESPOND: "Done! Your request has been processed."
THEN: complete
complete:
REASONING: false
RESPOND: "Thank you for using our service. Goodbye!"
```
### Entry point
The `entry_point:` property declares which step the flow begins at:
```yaml theme={null}
FLOW:
entry_point: greeting
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `entry_point` | `string` | No | First step in the `steps` list | The step name where execution begins |
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:
```yaml theme={null}
FLOW:
steps:
- greeting
- collect_account
- verify
- process
- complete
```
### Step definitions
Each step is defined as a named block under `FLOW:` with its properties indented:
```yaml theme={null}
FLOW:
greeting:
REASONING: false
RESPOND: "Hello! How can I help you today?"
THEN: collect_account
```
### 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.
```yaml theme={null}
FLOW:
analyze_request:
REASONING: true
GOAL: "Analyze the customer's request and determine the best course of action"
AVAILABLE_TOOLS: [search_knowledge, classify_intent]
EXIT_WHEN: intent_classified == true
MAX_TURNS: 5
THEN: route_request
route_request:
REASONING: false
CHECK: intent == "billing"
ON_FAIL: general_support
THEN: billing_flow
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `REASONING` | `boolean` | **Yes** | -- | `true` for LLM-driven reasoning, `false` for deterministic execution |
#### Reasoning step properties
When `REASONING: true`, the following additional properties are available:
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `GOAL` | `string` | No | Agent-level `GOAL:` | Step-specific goal (overrides the agent's goal for this step) |
| `AVAILABLE_TOOLS` | `string[]` | No | All agent tools | Subset of agent tools available in this step |
| `EXIT_WHEN` | `string` | No | *none* | Condition that ends the reasoning loop |
| `MAX_TURNS` | `number` | No | `10` | Maximum reasoning turns before forced exit |
| `STEP_CONSTRAINTS` | `string[]` | No | *none* | Constraints specific to this reasoning zone |
**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` should not have a `GOAL` (it has no effect on deterministic steps).
* A step with `REASONING: false` should not have `AVAILABLE_TOOLS` (use `CALL` to invoke tools deterministically).
### Entry guards
The `WHEN:` 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:
```yaml theme={null}
FLOW:
international_details:
REASONING: false
WHEN: "transfer_type == 'international'"
GATHER:
- swift_code: required
prompt: "What is the SWIFT/BIC code?"
THEN: validate_details
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `WHEN` | `string` | No | *none* | Condition expression that must be true for the step to execute |
### Attempt limiting
Steps can limit the number of times they execute (useful for retry loops):
```yaml theme={null}
FLOW:
collect_pin:
REASONING: false
MAX_ATTEMPTS: 3
ON_EXHAUSTED: lockout
GATHER:
- pin: required
prompt: "Enter your PIN."
THEN: verify_pin
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `MAX_ATTEMPTS` | `number` | No | *none* | Maximum number of times this step can execute |
| `ON_EXHAUSTED` | `string` | No | *none* | Step to transition to after max attempts exhausted |
***
## Step actions
### SAY / RESPOND
The `RESPOND:` action sends a message to the user. It supports template interpolation with `{{variable}}` syntax:
```yaml theme={null}
FLOW:
greeting:
REASONING: false
RESPOND: "Hello, {{customer_name}}! Your balance is {{balance}}."
THEN: next_step
```
Multi-line responses use pipe block syntax:
```yaml theme={null}
FLOW:
summary:
REASONING: false
RESPOND: |
Here is your transfer summary:
From: {{source_account}}
To: {{beneficiary_name}}
Amount: {{amount}} {{currency}}
THEN: confirm
```
#### 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:
```yaml theme={null}
FLOW:
show_products:
REASONING: false
RESPOND: "{{products_widget}}"
MODEL_CONTEXT: "Displayed products {{product_ids}}."
THEN: next_step
```
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](/agent-platform/abl-reference/tools#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:
```yaml theme={null}
FLOW:
review:
REASONING: false
PRESENT: |
Beneficiary: {{beneficiary_name}}
Account: {{MASK(beneficiary_account, "last4")}}
Bank: {{beneficiary_bank_name}}
GATHER:
- confirmation: required
THEN: process
```
### GATHER in flow steps
Within a flow step, `GATHER:` uses a list syntax different from the top-level `GATHER:` section:
```yaml theme={null}
FLOW:
collect_details:
REASONING: false
GATHER:
- source_account: required
prompt: "Your account number?"
THEN: next_step
```
#### Multi-field collection
Collect multiple fields in a single step using the `FIELDS:` sub-block:
```yaml expandable=true theme={null}
FLOW:
collect_beneficiary:
REASONING: false
GATHER:
FIELDS:
- beneficiary_name: required
prompt: "Full legal name on the account."
- beneficiary_account: required
prompt: "Account number or IBAN."
- beneficiary_bank: required
prompt: "Name of the bank."
- country: required
prompt: "Country of the bank."
default: "US"
infer: true
STRATEGY: llm
CORRECTIONS: true
COMPLETE_WHEN: beneficiary_name AND beneficiary_account AND beneficiary_bank AND country
THEN: validate
```
#### Flow gather field properties
Each field in a flow `GATHER:` supports these properties:
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `required` / optional value after name | `boolean` | No | `true` | Whether the field must be collected |
| `prompt` | `string` | No | *none* | Question shown to the user |
| `type` | `string` | No | `"string"` | Data type |
| `default` | `any` | No | *none* | Default value |
| `validation` | `string` | No | *none* | Validation expression |
| `infer` | `boolean` | No | `false` | Allow LLM inference |
| `infer_confidence` | `number` | No | `0.8` | Minimum inference confidence |
| `infer_confirm` | `boolean` | No | `true` | Confirm inferred values |
| `extraction_hints` | `string[]` | No | *none* | LLM extraction guidance |
| `range` | `boolean` | No | `false` | Collect as range |
| `list` | `boolean` | No | `false` | Collect as array |
| `activation` | `string` , `{when: string}` | No | *none* | Activation mode |
| `depends_on` | `string[]` | No | *none* | Field dependencies |
| `prompt_mode` | `"ask"` , `"extract_only"` | No | `"ask"` | Prompt usage mode |
Flow gather fields also accept the same additional properties as top-level
[GATHER](/agent-platform/abl-reference/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:
```yaml theme={null}
FLOW:
get_county:
REASONING: false
GATHER:
- county: required
prompt: "Which county is the dig site in?"
ASR:
vocabulary:
mode: replace
terms: [Guilford, Mecklenburg, Wake]
no_input_timeout_ms: 8000
get_street:
REASONING: false
GATHER:
- street_name: required
prompt: "What is the street name?"
ASR:
provider: deepgram
model: nova-3
vocabulary:
mode: extend
terms:
default: [Boulevard]
en: [{ phrase: Parkway, boost: 40 }]
es-MX: [Privada]
```
| Key | Meaning |
| - | - |
| `provider` | Speech engine for this field (for example `deepgram`, `google`, `azure`) — must already be configured on the project's voice channel. |
| `model` | Recognizer model for the field. Not validated at deploy; a model the engine rejects falls back to the normal settings. |
| `language` | Listening language for this question only — doesn't change the conversation language. |
| `vocabulary.mode` | `replace` (only this field's words), `extend` (the call's words plus this field's, field words kept first under a limit), or `none` (no special words). Without `vocabulary`, the call's words are unchanged. |
| `vocabulary.terms` | A plain list of words for every language, or a map of language tag (or `default`) to a list. Each entry is a string or `{ phrase, boost: 0-100 }`. The list is picked by exact tag, then base language, then `default` — lists are never merged across languages. |
| `no_input_timeout_ms` | Silence wait for this question, `1000`-`60000` ms. Has no effect when no-input handling is off for the channel. |
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.
#### GATHER block properties
Alongside `FIELDS:`, a flow `GATHER:` block accepts these block-level properties:
| Property | Type | Description |
| - | - | - |
| `STRATEGY` | `"auto"` , `"ml"` , `"llm"` , `"hybrid"` , `"pattern"` | Collection approach (see below). Also settable per field. |
| `PROMPT` | `string` | A single prompt used to collect the whole block. |
| `MESSAGE_KEY` | `string` | Locale catalog key for the block prompt. |
The `STRATEGY:` value controls the collection approach:
| Value | Description |
| - | - |
| `auto` | Let the platform choose the best available strategy |
| `ml` | Use the ML/NLU entity model |
| `llm` | Use LLM to extract values from natural conversation |
| `pattern` | Use pattern matching and extraction rules |
| `hybrid` | Combine ML and LLM extraction |
#### CORRECTIONS
When `CORRECTIONS: true`, the user can naturally correct previously collected values without restarting the step:
```yaml theme={null}
FLOW:
collect_info:
REASONING: false
GATHER:
- name: required
- email: required
CORRECTIONS: true
THEN: next_step
```
#### COMPLETE\_WHEN
The `COMPLETE_WHEN:` condition specifies when the gather step is considered complete:
```yaml theme={null}
FLOW:
collect_all:
REASONING: false
GATHER:
FIELDS:
- field_a: required
- field_b: required
- field_c: optional
COMPLETE_WHEN: field_a AND field_b
THEN: process
```
### CALL...WITH...AS
The `CALL:` action invokes a tool. Use `WITH:` to pass parameters and `AS:` to bind the result to a variable:
```yaml theme={null}
FLOW:
verify_step:
REASONING: false
CALL: verify_account
WITH:
account_id: source_account
AS: acctResult
RESPOND: "Account verified: {{acctResult.owner_name}}"
THEN: next_step
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `CALL` | `string` | -- | -- | Tool name to invoke |
| `WITH` | `Record` | No | *none* | Key-value parameter mapping |
| `AS` | `string` | No | *none* | Variable name to bind the tool result |
#### WITH parameters
The `WITH:` block maps tool parameter names to values or variable references:
```yaml theme={null}
CALL: calculate_fees
WITH:
transfer_type: transfer_type
amount: amount
currency: currency
destination_country: beneficiary_country
AS: feeResult
```
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:
```yaml theme={null}
CALL: ivr_digit_input
WITH:
prompt: "Enter your account number"
noInputConfig:
timeoutMs: 5000
maxRetries: 2
noMatchConfig:
message: "That didn't match. Please try again."
AS: digitResult
```
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//.json` or `locales//_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:
```yaml theme={null}
FLOW:
collect_card_number:
REASONING: false
CALL: ivr_digit_input
WITH:
prompt: "{{locale.CARD_PROMPT}}"
maxDigits: 19
endingKeyPress: "#"
noInputConfig:
timeout: 30
maxRetries: 2
message: "{{locale.CARD_NO_INPUT}}"
AS: cardNumberResult
ON_RESULT:
- IF: cardNumberResult.success == false
THEN: payment_input_failed
- ELSE:
THEN: validate_card_number
```
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:
```yaml theme={null}
FLOW:
init:
REASONING: false
SET:
status = "pending"
retry_count = 0
sanctions_clear = false
THEN: next_step
```
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:
```yaml theme={null}
SET:
filters = [{"field":"customer","operator":"in","value":["Public", "{{employer_name}}"]}]
```
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:
```yaml theme={null}
SET:
filters = [{"field":"customer","operator":"in","value":["Public", employer_name]}]
```
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`).
### CHECK
The `CHECK:` action evaluates a condition. If the condition is false, execution transitions to the `ON_FAIL:` step:
```yaml theme={null}
FLOW:
check_limits:
REASONING: false
CHECK: amount <= available_balance AND ADD(daily_used, amount) <= daily_limit
ON_FAIL: over_limit
THEN: proceed
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `CHECK` | `string` | -- | -- | Condition expression to evaluate |
| `ON_FAIL` | `string` | No | *none* | Step to go to if the condition is false |
### CLEAR
The `CLEAR:` action removes variables from the session context:
```yaml theme={null}
FLOW:
reset_beneficiary:
REASONING: false
CLEAR: [beneficiary_name, beneficiary_account, beneficiary_id]
THEN: collect_beneficiary
```
### TRANSFORM
The `TRANSFORM:` action filters, maps, sorts, and limits an array from the session context:
```yaml theme={null}
FLOW:
filter_results:
REASONING: false
TRANSFORM:
SOURCE: search_results
AS: hotel
INTO: filtered_hotels
FILTER: "hotel.rating >= 4"
MAP:
name: hotel.name
price: hotel.price_per_night
rating: hotel.rating
SORT_BY: price asc
LIMIT: 5
THEN: show_results
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `SOURCE` | `string` | Yes | -- | Dot-path to the source array |
| `AS` | `string` | Yes | -- | Loop variable name |
| `INTO` | `string` | Yes | -- | Output variable name |
| `FILTER` | `string` | No | *none* | Condition expression using the loop variable |
| `MAP` | `Record` | No | *none* | Field mapping expressions |
| `SORT_BY` | `string` | No | *none* | Sort field and direction (`asc` or `desc`) |
| `LIMIT` | `number` | No | *none* | Maximum number of items in the output |
### LOG
The `LOG:` action emits a trace-only diagnostic (not shown to the user). Use the inline or block form:
```yaml theme={null}
FLOW:
audit:
REASONING: false
LOG: "Entering audit step"
# or, with a level:
# LOG:
# MESSAGE: "Balance below threshold"
# LEVEL: warn # debug | info | warn | error
THEN: next_step
```
### AWAIT\_ATTACHMENT
The `AWAIT_ATTACHMENT:` action pauses the step until the user uploads a file.
```yaml theme={null}
FLOW:
collect_id_doc:
REASONING: false
AWAIT_ATTACHMENT:
NAME: id_document
PROMPT: "Please upload a photo of your ID."
CATEGORY: image # image | document | audio | video
REQUIRED: true
TIMEOUT: 120
ON_TIMEOUT: manual_review
THEN: verify_id
```
| Property | Type | Description |
| - | - | - |
| `NAME` | `string` | Variable the uploaded attachment is stored under. |
| `PROMPT` | `string` | Message asking the user to upload. |
| `CATEGORY` | `string` | `image`, `document`, `audio`, or `video`. |
| `REQUIRED` | `boolean` | Whether the upload is mandatory. |
| `TIMEOUT` | `number` | Seconds to wait for the upload. |
| `ON_TIMEOUT` | `string` | Step to transition to if the upload times out. |
### 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 (off by default): without a live voice session the step takes `ON_FAIL`; with the setting off, the step is skipped entirely. 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.
```yaml theme={null}
FLOW:
collect_card:
REASONING: false
SECURE_INPUT:
MODE: [dtmf] # dtmf | speech, one or both
FIELDS:
- card_number:
PROMPT: { en: "Please enter your 16-digit card number." }
COLLECT: { digits: 13..19, finishOnKey: "#" }
VALIDATE: [ luhn ]
- cvv:
PROMPT: { en: "And the 3-digit security code." }
COLLECT: { digits: 3..4 }
MAX_ATTEMPTS: 3
CANCEL: { dtmf_key: "*" }
CALL charge_card WITH: { pan: "{{secure.card_number}}", cvv: "{{secure.cvv}}" }
AS: payment_result
RESULT_FIELDS: [Status, TransactionIdentifier]
SUCCESS_WHEN: { Status: "Success" }
ON_SUCCESS: confirm
ON_TIMEOUT: retry_by_agent
ON_CANCEL: retry_by_agent
ON_FAIL: human_agent
THEN: confirm
confirm:
REASONING: false
RESPOND: "Your payment is complete. Reference: {{payment_result.TransactionIdentifier}}."
```
| Property | Type | Description |
| - | - | - |
| `MODE` | `("dtmf" \| "speech")[]` | Which collection channel(s) are accepted. Required; at least one. |
| `FIELDS` | list | One or more fields to collect, in order. Each is a `- :` entry with `PROMPT`, `COLLECT`, and optional `VALIDATE`. |
| `MAX_ATTEMPTS` | `number` | Failed collection attempts allowed before `ON_FAIL` (default 3). |
| `CANCEL` | `object` | Cancellation trigger: `dtmf_key` and/or `keywords` (a keyword must be spoken as its own word). |
| `CALL WITH: {...}` | — | The **delivery** call — the single tool invoked with the collected values. Only the inline-object `WITH:` form is supported. Reference a collected field as a whole-value placeholder `{{secure.}}`. |
| `AS` | `string` | Session variable that receives the delivery response. Requires `RESULT_FIELDS`. |
| `RESULT_FIELDS` | `string[]` (with `AS`) | Top-level response fields that may be bound to `AS`; nothing else in the response is ever read. Exact top-level keys only — nested paths aren't supported. |
| `SUCCESS_WHEN` | `object` | `{ Field: "value" }` or `{ Field: [value1, value2] }` — which delivery responses count as success. See below. |
| `ON_SUCCESS` | `string` | Step to transition to after successful collection and delivery. |
| `ON_TIMEOUT` | `string` | Step to transition to if collection times out. |
| `ON_CANCEL` | `string` | Step to transition to if the caller cancels. |
| `ON_FAIL` | `string` | Step to transition to if validation runs out of attempts, delivery fails, or `SUCCESS_WHEN` doesn't match. |
`AS`, `RESULT_FIELDS`, and `SUCCESS_WHEN` may be written indented under the `CALL` line or beside it; `AS: ` 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
| Property | Type | Description |
| - | - | - |
| `PROMPT` | `{ : string }` or `{ template: string }` | Locale-keyed prompt text, or a reference to a named template. At least one non-empty entry is required. A locale reference such as `"{{locale.KEY}}"` also works. |
| `COLLECT` | `object` | `digits: N..M` (min/max digit count), `finishOnKey`, `interDigitTimeoutMs`, `asrTimeoutMs` (spoken-path silence window). |
| `VALIDATE` | list | Validation rules: a bare rule name (`luhn`), an inline object (`{ length: 4..4 }`, `{ date_mmYY: "not_past" }`), or a delegated `CALL WITH: {...}` rule. |
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.
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.
| Diagnostic | Fires when |
| - | - |
| `SECURE_INPUT_INVALID_MODE` | `MODE` is missing, empty, or contains a value other than `dtmf`/`speech`. |
| `SECURE_INPUT_MISSING_DELIVER_TOOL` | No delivery `CALL`. |
| `SECURE_INPUT_MISSING_PROMPT` | No `FIELDS` are declared, or a field's `PROMPT` has neither a locale entry nor a `template` reference. |
| `SECURE_EMBEDDED_PLACEHOLDER` | A `{{secure.*}}` reference is part of a longer argument string instead of the whole value. |
| `SECURE_UNKNOWN_FIELD` | A `{{secure.*}}` reference names a field the step doesn't collect. |
| `SECURE_INPUT_EMPTY_OUTCOME_ROUTE` | An outcome route (`ON_SUCCESS`/`ON_FAIL`/`ON_TIMEOUT`/`ON_CANCEL`) is written without a step name. |
| `SECURE_INPUT_INVALID_RESULT_CONTRACT` | `AS` that isn't a plain variable name, `AS` without `RESULT_FIELDS` (or the reverse), a nested or repeated field name, an empty `SUCCESS_WHEN`, or a `SUCCESS_WHEN` field with no value. |
| `RESERVED_VARIABLE_NAME` / `IMMUTABLE_SYSTEM_IDENTIFIER` | `AS` names a reserved or system-owned variable such as `session_id`. |
| `W208` / `W210` | A line inside the block, or a field property, that `SECURE_INPUT` doesn't read — it's ignored. |
| `W211` | `RESULT_FIELDS` or `SUCCESS_WHEN` written across several lines; write them inline. |
### 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.
```yaml theme={null}
FLOW:
resolve_destination:
REASONING: false
RESOLVE_SIP_DESTINATION:
FROM_WORKFLOW: "lookup_vdn_by_region_language" # or: FROM_MEMORY: routing_decision
TIMEOUT_MS: 1500
THEN: transfer_step
transfer_step:
REASONING: false
CALL: call_transfer(callTransferType: "sip")
THEN: COMPLETE
```
| Sub-key | Type | Description |
| - | - | - |
| `FROM_WORKFLOW` | `string` | Name of a workflow tool invoked as the destination feeder. |
| `FROM_MEMORY` | `string` | Session memory key to read as the feeder (use instead of `FROM_WORKFLOW`). |
| `TIMEOUT_MS` | `number` | Hard timeout for the feeder, in milliseconds. |
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](/agent-platform/abl/reference/tools#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:
```yaml theme={null}
FLOW:
check_status:
REASONING: false
CHECK: account_status == "active"
ON_FAIL: inactive_account
THEN: proceed
```
`ON_FAIL:` also accepts a structured block for recovery flows — `COLLECT:` (fields to re-collect),
`GOTO:` (step), `RETRY: true`, `RESPOND:`, and `THEN:`:
```yaml theme={null}
FLOW:
validate_details:
REASONING: false
CHECK: routing_number_valid == true
ON_FAIL:
RESPOND: "That routing number looks off — let's try again."
COLLECT: [routing_number]
GOTO: collect_routing
```
### 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:
```yaml expandable=true theme={null}
FLOW:
verify_step:
REASONING: false
CALL: verify_account
WITH:
account_id: source_account
AS: acctResult
ON_RESULT:
- IF: acctResult.status == "active"
SET:
customer_name = acctResult.owner_name
balance = acctResult.available_balance
RESPOND: "Account verified. Hello, {{customer_name}}."
THEN: next_step
- IF: acctResult.status == "frozen"
RESPOND: "Your account is frozen. Please contact your branch."
THEN: end
- ELSE:
RESPOND: "Account not found. Please check the number."
THEN: collect_account
```
#### ON\_RESULT branch properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `IF` | `string` | No | -- | Condition expression. Omit for `ELSE` branch. |
| `RESPOND` | `string` | No | *none* | Message to display |
| `SET` | `Record` | No | *none* | Variable assignments |
| `CALL` | `string` | No | *none* | Optional nested tool call |
| `THEN` | `string` | Yes | -- | Next step |
The `ELSE` branch (a branch with no `IF` condition) matches when no other condition is true.
### ON\_SUCCESS / ON\_FAIL
An alternative to `ON_RESULT:` for simpler success/failure branching:
```yaml theme={null}
FLOW:
call_api:
REASONING: false
CALL: submit_request
WITH:
data: request_data
AS: result
ON_SUCCESS:
RESPOND: "Request submitted: {{result.id}}"
THEN: done
ON_FAIL:
RESPOND: "Request failed. Please try again."
THEN: retry_step
```
Both `ON_SUCCESS:` and `ON_FAIL:` support conditional branches:
```yaml expandable=true theme={null}
FLOW:
process:
REASONING: false
CALL: process_order
AS: orderResult
ON_SUCCESS:
- IF: orderResult.express == true
RESPOND: "Express order confirmed!"
THEN: express_fulfillment
- ELSE:
RESPOND: "Order confirmed."
THEN: standard_fulfillment
ON_FAIL:
- IF: orderResult.error == "out_of_stock"
RESPOND: "Item is out of stock."
THEN: suggest_alternatives
- ELSE:
RESPOND: "An error occurred."
THEN: error_handling
```
### ON\_INPUT
`ON_INPUT:` provides branching based on user input, typically used after a `GATHER:` action:
```yaml theme={null}
FLOW:
confirm:
REASONING: false
GATHER:
- confirmation: required
ON_INPUT:
- IF: input.contains("yes") OR input.contains("confirm")
THEN: execute
- IF: input.contains("no") OR input.contains("cancel")
RESPOND: "Cancelled."
THEN: cleanup
- ELSE:
RESPOND: "Please type 'yes' to confirm or 'no' to cancel."
THEN: confirm
```
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.
### Digressions
Digressions are intent-based escapes that can interrupt the current step. They match user intent patterns and respond accordingly:
```yaml expandable=true theme={null}
FLOW:
collect_details:
REASONING: false
GATHER:
- account: required
DIGRESSIONS:
- INTENT: "what is swift"
RESPOND: "A SWIFT code is an 8 or 11 character code identifying a bank internationally."
RESUME: true
- INTENT: "cancel"
RESPOND: "Transaction cancelled."
GOTO: cleanup
- INTENT: "speak to agent"
RESPOND: "Connecting you with a specialist."
DELEGATE: Live_Agent
```
#### Digression properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `INTENT` | `string` | Yes | -- | Intent pattern to match |
| `KEYWORDS` | `string[]` | No | *none* | Explicit keywords for matching |
| `CONDITION` | `string` | No | *none* | Additional condition to check |
| `RESPOND` | `string` | No | *none* | Response message |
| `MESSAGE_KEY` | `string` | No | *none* | Locale catalog key for the response |
| `SET` | `Record` | No | *none* | Variable assignments (within the `DO:` block) |
| `RESUME` | `boolean` | No | `false` | Return to the current step after handling |
| `GOTO` | `string` | No | *none* | Transition to a specific step |
| `HANDOFF` | `string` | No | *none* | Hand off to another agent (supports `RETURN:` / `ON_RETURN:`) |
| `DELEGATE` | `string` | No | *none* | Delegate to another agent (requires `RETURN: true`) |
| `CALL` | `string` | No | *none* | Tool to invoke |
| `CLEAR` | `string[]` | No | *none* | Variables to clear |
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.
```yaml theme={null}
DIGRESSIONS:
- INTENT: "check balance"
DO:
- RESPOND: "Let me pull that up."
- HANDOFF: Account_Info
RETURN: true
- RESUME: true
```
### Global digressions
Digressions declared at the flow level (under `global_digressions:`) are available at every step:
```yaml theme={null}
FLOW:
global_digressions:
- INTENT: "cancel"
RESPOND: "Transaction cancelled."
GOTO: cleanup
- INTENT: "help"
RESPOND: "I can help with account inquiries, transfers, and balance checks."
RESUME: true
- INTENT: "speak to agent"
RESPOND: "Connecting you with a live agent."
DELEGATE: Live_Agent_Transfer
```
### Sub-intents
Sub-intents are scoped intents valid only within a specific step. They handle step-specific user requests like corrections or clarifications:
```yaml expandable=true theme={null}
FLOW:
collect_beneficiary:
REASONING: false
GATHER:
FIELDS:
- name: required
- account: required
- bank: required
SUB_INTENTS:
- INTENT: "change name"
CLEAR: [name]
RESPOND: "What is the correct name?"
- INTENT: "change account"
CLEAR: [account]
RESPOND: "What is the correct account number?"
- INTENT: "change bank"
CLEAR: [bank, routing_code]
RESPOND: "What is the correct bank name?"
```
#### Sub-intent properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `INTENT` | `string` | Yes | -- | Intent pattern to match |
| `RESPOND` | `string` | No | *none* | Response message |
| `MESSAGE_KEY` | `string` | No | *none* | Locale catalog key for the response |
| `CLEAR` | `string[]` | No | *none* | Variables to clear (triggers re-collection) |
| `SET` | `Record` | No | *none* | Variables to set |
| `CALL` | `string` | No | *none* | Tool to invoke |
| `RESUME` | `boolean` | No | `true` | Stay in the current step |
***
## 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:
```yaml expandable=true theme={null}
FLOW:
choose_option:
REASONING: false
RESPOND: "How would you like to proceed?"
ACTIONS:
- id: "option_transfer"
type: button
label: "Wire Transfer"
value: "wire"
- id: "option_ach"
type: button
label: "ACH Transfer"
value: "ach"
- id: "option_check"
type: button
label: "Send a Check"
value: "check"
ON_ACTION:
- ACTION: "option_transfer"
SET:
transfer_method = "wire"
THEN: wire_flow
- ACTION: "option_ach"
SET:
transfer_method = "ach"
THEN: ach_flow
- ACTION: "option_check"
SET:
transfer_method = "check"
THEN: check_flow
```
### Element types
#### Buttons
```yaml theme={null}
ACTIONS:
- id: "confirm_btn"
type: button
label: "Confirm"
value: "confirmed"
```
#### Select (dropdown)
```yaml theme={null}
ACTIONS:
- id: "currency_select"
type: select
label: "Select currency"
options:
- id: "usd"
label: "US Dollar"
description: "United States Dollar"
- id: "eur"
label: "Euro"
description: "European Union Euro"
- id: "gbp"
label: "British Pound"
```
#### Input fields
```yaml theme={null}
ACTIONS:
- id: "amount_input"
type: input
label: "Enter amount"
inputType: number
placeholder: "0.00"
required: true
```
### Action element properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `id` | `string` | Yes | -- | Unique identifier for the action element |
| `type` | `"button"` , `"select"` , `"input"` | Yes | -- | Element type |
| `label` | `string` | Yes | -- | Display label |
| `value` | `string` | No | *none* | Hidden value sent when the element is interacted with |
| `description` | `string` | No | *none* | Subtitle or helper text |
| `options` | `{id, label, description?}[]` | No | *none* | Options for `select` type |
| `inputType` | `"text"` , `"number"` , `"date"` , `"time"` , `"email"` | No | `"text"` | Input field type (for `input` type) |
| `placeholder` | `string` | No | *none* | Placeholder text (for `input` type) |
| `required` | `boolean` | No | `false` | Whether the input is required (for `input` type) |
### Form submission
When a step contains `input` type elements, you can configure a form submission button:
```yaml theme={null}
ACTIONS:
- id: "name_input"
type: input
label: "Your name"
inputType: text
required: true
- id: "email_input"
type: input
label: "Your email"
inputType: email
required: true
SUBMIT_LABEL: "Submit"
SUBMIT_ID: "form_submit"
```
The `ACTIONS:` block also accepts an optional `RENDER_ID:` to give the whole action set a stable
identifier for rendering/telemetry.
### ON\_ACTION callbacks
The `ON_ACTION:` block defines handlers for user interactions with action elements:
```yaml theme={null}
ON_ACTION:
- ACTION: "confirm_btn"
RESPOND: "Confirmed! Processing your request."
SET:
confirmed = true
THEN: process_step
- ACTION: "cancel_btn"
RESPOND: "Cancelled."
THEN: cleanup
```
#### ON\_ACTION handler properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `ACTION` | `string` | Yes | -- | The `id` of the action element that triggered this handler |
| `CONDITION` | `string` | No | *none* | Optional value-based condition |
| `RESPOND` | `string` | No | *none* | Response message |
| `SET` | `Record` | No | *none* | Variable assignments |
| `CLEAR` | `string[]` | No | *none* | Variables to clear |
| `CALL` | `string` | No | *none* | Tool to invoke (requires `AS`) |
| `LOG` | `string` | No | *none* | Trace-only diagnostic |
| `HANDOFF` | `string` | No | *none* | Hand off to another agent |
| `DELEGATE` | `string` | No | *none* | Delegate to another agent (`RETURN:`/`ON_RETURN:` supported) |
| `COMPLETE` | `boolean` | No | *none* | Terminal — mark the flow complete |
| `THEN` / `GOTO` / `TRANSITION` | `string` | No | *none* | Step to transition to (three interchangeable keywords) |
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.
```yaml theme={null}
FLOW:
show_summary:
REASONING: false
RESPOND: "Here is your transfer summary."
FORMATS:
MARKDOWN: |
## Wire Transfer Summary
| Field | Value |
|-------|-------|
| From | {{source_account}} |
| To | {{beneficiary_name}} |
| Amount | {{amount}} {{currency}} |
ADAPTIVE_CARD: |
{"type": "AdaptiveCard", "body": [...]}
```
#### 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](/agent-platform/abl-reference/rich-content) for their schemas):
| Format | Description |
| - | - |
| `MARKDOWN` | Formatted markdown text |
| `ADAPTIVE_CARD` | Microsoft Adaptive Cards JSON |
| `HTML` | HTML content |
| `SLACK` | Slack Block Kit JSON |
| `AG_UI` | AG-UI / CopilotKit event JSON |
| `WHATSAPP` | WhatsApp interactive message JSON |
| `CAROUSEL` | Scrollable cards (title/subtitle/image/buttons); can also be authored as a standalone block after `RESPOND:` |
| `QUICK_REPLIES` | Quick-reply chips |
| `LIST` | Vertical list of items |
| `IMAGE` | Image with caption/alt |
| `VIDEO` | Video with caption/alt |
| `AUDIO` | Audio with caption/alt |
| `FILE` | File attachment |
| `KPI` | KPI metric tile |
| `TABLE` | Columns + rows |
| `CHART` | Bar/line/pie chart |
| `FORM` | Form fields + submit |
| `PROGRESS` | Progress bar/circle |
| `FEEDBACK` | Thumbs/stars/scale feedback |
### Voice configuration
Steps can include voice-specific overrides for text-to-speech engines:
```yaml theme={null}
FLOW:
greeting:
REASONING: false
RESPOND: "Welcome to wire transfer services."
VOICE:
SSML: |
Welcome to wire transfer services.
INSTRUCTIONS: "Speak in a warm, professional tone."
PLAIN_TEXT: "Welcome to wire transfer services."
```
| Property | Type | Description |
| - | - | - |
| `SSML` | `string` | W3C SSML markup for TTS engines |
| `INSTRUCTIONS` | `string` | Natural language voice style instructions |
| `PLAIN_TEXT` | `string` | Voice-optimized plaintext (fallback for all engines) |
| `BARGE_IN` | `"allow"` \| `"disallow"` | Whether the caller may interrupt (barge in on) this spoken output. |
| `STREAMING` | `boolean` | TTS delivery mode for this spoken output. Omitted defaults to `true` (streaming). |
| `END_OF_SPEECH_TURN_TYPE` | `string` | Expected shape of the caller's next answer, used to pick an end-of-speech (pause) timeout. See [Per-turn endpointing](#per-turn-endpointing). |
| `END_OF_SPEECH_TIMEOUT_MS` | `number` | Explicit end-of-speech timeout in milliseconds, overriding the preset. |
#### 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:
| Value | Use for |
| - | - |
| `simple_confirmation` | A yes/no or short acknowledgement. |
| `structured_value` | A structured value like an account number or date. |
| `open_ended_question` | An open-ended question expecting a longer, free-form answer. |
| `complex_explanation` | A caller explanation that may take a while to finish. |
| `anything_else` | Anything not covered above. |
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`:
```yaml theme={null}
FLOW:
collect_value:
REASONING: false
VOICE:
END_OF_SPEECH_TURN_TYPE: structured_value
END_OF_SPEECH_TIMEOUT_MS: 900
RESPOND: "Please provide the value."
VOICE:
END_OF_SPEECH_TURN_TYPE: open_ended_question # per-RESPOND override
END_OF_SPEECH_TIMEOUT_MS: 1400
```
`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:
```yaml theme={null}
FLOW:
notice:
REASONING: false
BARGE_IN: disallow # step-wide: applies to this step's outputs
RESPOND: "This required notice must play in full."
VOICE:
BARGE_IN: allow # per-RESPOND override for this output
```
It can also be set on a named template's `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.
```yaml theme={null}
FLOW:
read_confirmation_code:
REASONING: false
STREAMING: false # step-wide: wait for the full utterance before speaking it
RESPOND: "Your confirmation code is {{confirmation_code}}."
VOICE:
STREAMING: false # equivalent per-RESPOND override
```
Omitting `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 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:
```yaml theme={null}
CONVERSATION:
listening:
on_no_input:
enabled: true
mode: static
timeout_ms: 5000
max_retries: 3
messages:
- text: "Are you still there?"
- text: "Hello?"
FLOW:
ask_account:
REASONING: false
RESPOND: "What's your account number?"
GATHER:
- account_number: required
VOICE:
ON_NO_INPUT:
timeout_ms: 10000
messages:
- text: "Could you tell me your account number, {{memo_name}}?"
THEN: complete
```
* **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.
`ON_NO_INPUT` is only legal 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.
***
## Step-level error handling
Steps can define local error handlers that override agent-level `ON_ERROR:` handlers:
```yaml expandable=true theme={null}
FLOW:
call_external:
REASONING: false
CALL: external_api
WITH:
query: user_query
AS: apiResult
ON_ERROR:
tool_error:
RESPOND: "The service is responding slowly. Retrying..."
SUBTYPE: tool_timeout
RETRY: 2
RETRY_DELAY: 1000
RETRY_BACKOFF: exponential
RETRY_MAX_DELAY: 8000
THEN: CONTINUE
validation_error:
RESPOND: "An error occurred. Let me try a different approach."
THEN: RETRY_STEP
```
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](/agent-platform/abl-reference/lifecycle-and-hooks#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
| Property | Type | Description |
| - | - | - |
| `SUBTYPE` | `string` | Narrow the handler to a specific error subtype. |
| `RESPOND` | `string` | Message to show when the error fires. |
| `RETRY` | `number` | Retry attempts before giving up. |
| `RETRY_DELAY` | `number` | Delay (ms) between retries. |
| `RETRY_BACKOFF` | `string` | Backoff strategy: `fixed`, `exponential`, or `linear`. |
| `RETRY_MAX_DELAY` | `number` | Maximum delay (ms) between retries. |
| `BACKTRACK_TO` | `string` | Step to backtrack to (used with `THEN: BACKTRACK`). |
| `HANDOFF_TARGET` | `string` | Target agent (used with `THEN: HANDOFF`). |
| `THEN` | `string` | Next action: `CONTINUE`, `COMPLETE`, `ESCALATE`, `BACKTRACK`, `RETRY_STEP`, or `HANDOFF`. A step name is **not** a valid value here — a flow step's own `THEN:` navigates to a step, but an `ON_ERROR` handler's `THEN:` takes only one of these six actions. |
## Complete example
```yaml expandable=true theme={null}
AGENT: Simple_Order
VERSION: "1.0.0"
GOAL: "Help customers place orders."
PERSONA: "Friendly order assistant."
TOOLS:
check_inventory(product_id: string) -> {available: boolean, quantity: number}
description: "Check product availability"
type: http
endpoint: "/api/inventory/check"
method: GET
place_order(product_id: string, quantity: number) -> {order_id: string, total: number}
description: "Place an order"
type: http
endpoint: "/api/orders"
method: POST
FLOW:
entry_point: welcome
steps:
- welcome
- collect_product
- check_stock
- out_of_stock
- collect_quantity
- confirm
- place
- done
global_digressions:
- INTENT: "cancel"
RESPOND: "Order cancelled."
GOTO: done
welcome:
REASONING: false
RESPOND: "Welcome! What product are you looking for?"
THEN: collect_product
collect_product:
REASONING: false
GATHER:
- product_id: required
prompt: "What is the product ID?"
THEN: check_stock
check_stock:
REASONING: false
CALL: check_inventory
WITH:
product_id: product_id
AS: stockResult
ON_RESULT:
- IF: stockResult.available == true
SET:
stock_quantity = stockResult.quantity
RESPOND: "Great news! We have {{stock_quantity}} in stock."
THEN: collect_quantity
- ELSE:
THEN: out_of_stock
out_of_stock:
REASONING: false
RESPOND: "Sorry, that product is currently out of stock."
THEN: done
collect_quantity:
REASONING: false
GATHER:
- quantity: required
type: number
prompt: "How many would you like to order?"
CHECK: quantity <= stock_quantity
ON_FAIL: collect_quantity
THEN: confirm
confirm:
REASONING: false
RESPOND: "You'd like to order {{quantity}} of {{product_id}}. Confirm?"
ACTIONS:
- id: "yes"
type: button
label: "Confirm"
- id: "no"
type: button
label: "Cancel"
ON_ACTION:
- ACTION: "yes"
THEN: place
- ACTION: "no"
RESPOND: "Order cancelled."
THEN: done
place:
REASONING: false
CALL: place_order
WITH:
product_id: product_id
quantity: quantity
AS: orderResult
ON_SUCCESS:
RESPOND: "Order placed! Order ID: {{orderResult.order_id}}, Total: {{orderResult.total}}"
THEN: done
ON_FAIL:
RESPOND: "Failed to place order. Please try again later."
THEN: done
done:
REASONING: false
RESPOND: "Thank you for using our service!"
```
***
**Related articles:**
* [Language overview](/agent-platform/abl-reference/language-overview) -- syntax rules and auto-detection
* [Tools](/agent-platform/abl-reference/tools) -- tool definitions used by `CALL` actions
* [GATHER](/agent-platform/abl-reference/gather) -- top-level gather field definitions
* [Agent declaration](/agent-platform/abl-reference/agent-declaration) -- GOAL used by `REASONING: true` steps, `max_flow_iterations` and model settings
# GATHER (information collection)
Source: https://koreai.mintlify.app/agent-platform/abl/reference/gather
The `GATHER:` section defines structured information that the agent needs to collect from the user during a conversation. Each field specifies a data type, prompt, validation rules, and collection behavior. The runtime orchestrates multi-turn collection, extracting values from natural conversation and prompting for missing fields.
## Syntax
At the top level of an agent document, `GATHER:` defines fields as named blocks:
```yaml expandable=true theme={null}
GATHER:
source_account:
prompt: "Which account would you like to use?"
type: string
required: true
amount:
prompt: "How much would you like to transfer?"
type: number
required: true
validate: min(1)
currency:
prompt: "What currency?"
type: string
required: true
default: "USD"
infer: true
```
Within a `FLOW:` step, gather uses a different syntax. See [GATHER within FLOW](/agent-platform/abl-reference/flow#gather-in-flow-steps).
## Field properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `prompt` | `string` | Yes | -- | The question or instruction shown to the user to collect this field |
| `message_key` | `string` | No | *none* | Locale catalog key used to localize the `prompt` |
| `description` | `string` | No | *none* | Semantic, model-facing description of what the value represents (distinct from `prompt`). Overrides a referenced entity's description. |
| `type` | `string` | No | `"string"` | Data type for the field value (see [Field types](#field-types)) |
| `required` | `boolean` | No | `true` | Whether the field must be collected before proceeding |
| `default` | `any` | No | *none* | Default value used if the user does not provide one |
| `entity_ref` | `string` | No | *none* | Bind to a named entity from the `ENTITIES:` section (see [Entity binding](#entity-binding)) |
| `options` | `string[]` | No | *none* | Allowed values for enum-style fields (see [Enum options](#enum-options)) |
| `validate` | `string` | No | *none* | Validation expression (see [Validation](#validation)) |
| `validation_process` | `"REGEX"` , `"CODE"` , `"LLM"` | No | *none* | How validation is performed |
| `retry_prompt` | `string` | No | *none* | Custom prompt shown when validation fails |
| `max_retries` | `number` | No | *none* | Maximum validation retry attempts |
| `infer` | `boolean` | No | `false` | Allow the LLM to infer the value from context |
| `infer_confidence` | `number` | No | `0.8` | Minimum confidence threshold for accepting inferred values (0.0--1.0) |
| `infer_confirm` | `boolean` | No | `true` | Whether to confirm inferred values with the user |
| `prompt_mode` | `"ask"` , `"extract_only"` | No | `"ask"` | Whether the prompt is shown to the user or used only for LLM extraction |
| `range` | `boolean` | No | `false` | Collect as a range `{low, high}` instead of a scalar |
| `list` | `boolean` | No | `false` | Collect as an array instead of a scalar |
| `preferences` | `boolean` | No | `false` | Categorize into accept/desire/avoid/refuse preference sets |
| `activation` | `string` , `{when: string}` | No | `"required"` | Activation mode (see [Progressive activation](#progressive-activation)) |
| `depends_on` | `string[]` | No | *none* | Fields that must be collected before this field activates |
| `satisfied_by` | `string[]` | No | *none* | Provenance classes allowed to satisfy this field (see [Value provenance](#value-provenance)) |
| `sensitive` | `boolean` | No | `false` | Whether this field carries PII |
| `sensitive_display` | `"redact"` , `"mask"` , `"replace"` | No | *none* | How sensitive values are displayed outside the gather context |
| `mask_config` | `{showFirst, showLast, char}` | No | *none* | Masking configuration when `sensitive_display` is `"mask"` |
| `pii_type` | `string` | No | *none* | Explicit PII classification for shape-preserving masking (see [Sensitive fields](#sensitive-fields)) |
| `pii_pattern_ref` | `string` | No | *none* | Name of a project-defined PII pattern to bind this field to, instead of `pii_type` (see [Sensitive fields](#sensitive-fields)) |
| `transient` | `boolean` | No | `false` | Whether PII auto-cleans after gather completes |
| `extraction_pattern` | `string` | No | *none* | Custom regex pattern for value extraction |
| `extraction_group` | `number` | No | `0` | Capture group index for `extraction_pattern` |
| `extraction_hints` | `string[]` | No | *none* | Example surface forms that guide LLM extraction. Merged on top of a referenced entity's hints; an explicit empty list clears inherited hints. |
| `strategy` | `"auto"` , `"ml"` , `"llm"` , `"hybrid"` , `"pattern"` | No | *none* | Per-field extraction strategy override (see [Extraction strategy](#extraction-strategy)). |
| `semantics` | `object` | No | *none* | Supplemental metadata about the value (see [Semantics](#semantics)) |
## Field types
The `type` property specifies the expected data type for the field value:
| Type | Description | Example values |
| - | - | - |
| `string` | Free-text value | `"John Smith"`, `"USD"` |
| `text` | Longer free-text value | `"..."` |
| `free_text` | Unconstrained free text | `"..."` |
| `number` | Numeric value | `42`, `150.75` |
| `integer` | Whole number | `42` |
| `float` | Decimal number | `3.14` |
| `currency` | Monetary amount | `19.99` |
| `boolean` | True/false | `true`, `false` |
| `date` | Calendar date | `"2026-03-15"` |
| `datetime` | Date and time | `"2026-03-15T14:30:00"` |
| `email` | Email address | `"user@example.com"` |
| `phone` | Phone number | `"+1-555-123-4567"` |
| `enum` | One of a fixed set (see `options`) | `"domestic"` |
| `pattern` | Value matching a regex | `"AB1234"` |
| `location` | Geographic location | `"San Francisco, CA"` |
## Entity binding
Instead of declaring `type`, `options`, and validation inline, a field can reference a reusable
named entity from the [`ENTITIES:`](/agent-platform/abl-reference/nlu) section via `entity_ref`. The
field then inherits the entity's type, allowed values, synonyms, and validation.
```yaml theme={null}
GATHER:
request:
entity_ref: request_type # defined in the ENTITIES: section
prompt: "What type of request do you have?"
```
A field with `entity_ref` should not also declare `type`, `options`, or values — the referenced
entity owns that contract.
## Enum options
For enum-style fields, `options` lists the allowed values directly. Use it as an alternative to
`validate: enum(...)`. Both inline and block forms are accepted:
```yaml theme={null}
GATHER:
plan:
prompt: "Which plan would you like?"
type: enum
options: [basic, pro, enterprise]
region:
prompt: "Which region?"
type: enum
options:
- "us-east"
- "eu-west"
```
## Value provenance
The `satisfied_by` property restricts which sources may satisfy a field — useful for fields that
must come from an authoritative system rather than user claims.
```yaml theme={null}
GATHER:
account_balance:
prompt: "What is your current balance?"
type: number
satisfied_by: [authoritative_tool, system]
```
| Value | Description |
| - | - |
| `user_input` | The value may be provided directly by the user. |
| `authoritative_tool` | The value may be supplied by an authoritative tool result. |
| `system` | The value may be set by the system/session. |
## Validation
The `validate` property defines a validation expression that the collected value must satisfy:
```yaml expandable=true theme={null}
GATHER:
amount:
prompt: "How much?"
type: number
required: true
validate: min(1)
email:
prompt: "Your email address?"
type: string
required: true
validate: email()
transfer_type:
prompt: "Domestic or international?"
type: string
required: true
validate: enum(domestic, international)
```
### Validation expressions
| Expression | Description | Example |
| - | - | - |
| `min(n)` | Minimum numeric value | `validate: min(1)` |
| `max(n)` | Maximum numeric value | `validate: max(10000)` |
| `enum(a, b, c)` | Must be one of the listed values | `validate: enum(domestic, international)` |
| `email()` | Must be a valid email format | `validate: email()` |
| `pattern(regex)` | Must match a regex pattern | `validate: pattern(^\d{9}$)` |
### Validation process
The `validation_process` property controls how validation is executed:
| Value | Description |
| - | - |
| `REGEX` | Validate using a regular expression pattern |
| `CODE` | Validate using a code expression |
| `LLM` | Validate using LLM judgment |
```yaml theme={null}
GATHER:
routing_number:
prompt: "What is the ABA routing number?"
type: string
required: true
validate: pattern(^\d{9}$)
validation_process: REGEX
retry_prompt: "That doesn't look like a valid 9-digit routing number. Please re-enter."
max_retries: 3
```
## LLM inference
When `infer: true`, the LLM can extract or infer the field value from conversational context without explicitly asking the user. This enables natural, conversational information collection.
```yaml expandable=true theme={null}
GATHER:
transfer_type:
prompt: "Is this domestic or international?"
type: string
required: true
validate: enum(domestic, international)
infer: true
currency:
prompt: "What currency?"
type: string
required: true
default: "USD"
infer: true
infer_confidence: 0.9
infer_confirm: false
```
### Inference properties
| Property | Type | Default | Description |
| - | - | - | - |
| `infer` | `boolean` | `false` | Enable LLM inference for this field |
| `infer_confidence` | `number` | `0.8` | Minimum confidence (0.0--1.0) the LLM must have to accept an inferred value |
| `infer_confirm` | `boolean` | `true` | Whether to ask the user to confirm inferred values |
When the LLM's confidence in an inferred value falls below `infer_confidence`, the runtime falls back to asking the user directly using the `prompt`.
Use `extraction_hints` to steer extraction with example surface forms:
```yaml theme={null}
GATHER:
transfer_type:
prompt: "Is this domestic or international?"
type: string
infer: true
extraction_hints: ["If beneficiary country is US, infer domestic. Otherwise international."]
```
## Extraction strategy
The `strategy` property selects how a value is extracted. It can be set per field, and the same
enum is available as a block-level default on flow-step gather.
| Value | Behavior |
| - | - |
| `auto` | Let the platform choose the best available strategy (default behavior when unset). |
| `ml` | Use the ML/NLU entity model. |
| `llm` | Use the LLM to extract the value. |
| `pattern` | Use pattern/regex extraction (pairs with `extraction_pattern`). |
| `hybrid` | Combine ML and LLM extraction. |
```yaml theme={null}
GATHER:
phone_number:
prompt: "What is your phone number?"
type: phone
strategy: pattern
extraction_pattern: "\\d{3}-\\d{3}-\\d{4}"
```
## Range collection
When `range: true`, the field collects a range with `low` and `high` values instead of a single scalar:
```yaml theme={null}
GATHER:
budget:
prompt: "What is your budget range?"
type: number
required: true
range: true
```
The collected value is stored as an object: `{low: number, high: number}`.
## List collection
When `list: true`, the field collects multiple values as an array:
```yaml theme={null}
GATHER:
dietary_restrictions:
prompt: "Do you have any dietary restrictions?"
type: string
required: false
list: true
```
The collected value is stored as an array: `["vegetarian", "no nuts"]`.
## Preference collection
When `preferences: true`, the field categorizes user input into preference sets:
```yaml theme={null}
GATHER:
hotel_amenities:
prompt: "What amenities are important to you?"
type: string
required: false
preferences: true
```
The collected value is categorized into: `{accept: [], desire: [], avoid: [], refuse: []}`.
## Progressive activation
The `activation` property controls when a field becomes active for collection:
| Value | Description |
| - | - |
| `"required"` | Always active, must be collected (default) |
| `"optional"` | Active but not required |
| `"progressive"` | Activated progressively as other fields are collected |
| `{when: "condition"}` | Activated only when the condition evaluates to true |
```yaml theme={null}
GATHER:
routing_number:
prompt: "What is the ABA routing number?"
type: string
required: true
activation: required
depends_on: [transfer_type]
swift_code:
prompt: "What is the SWIFT/BIC code?"
type: string
required: true
activation:
WHEN: "transfer_type == 'international'"
depends_on: [transfer_type]
```
### Field dependencies
The `depends_on` property lists fields that must be collected before this field activates. Combined with `activation`, this creates ordered collection flows:
```yaml theme={null}
GATHER:
destination:
prompt: "Where are you traveling?"
type: string
required: true
hotel_class:
prompt: "What class of hotel?"
type: string
required: true
depends_on: [destination]
activation: progressive
```
## Corrections handling
In flow steps, the `CORRECTIONS: true` property enables natural corrections -- the user can update previously collected values by stating corrections in natural language:
```yaml theme={null}
# Within a FLOW step
collect_details:
GATHER:
- name: required
- email: required
CORRECTIONS: true
```
See [FLOW](/agent-platform/abl-reference/flow) for the full flow step GATHER syntax.
## Transient and sensitive fields
### Sensitive fields
Fields marked `sensitive: true` carry PII and receive special handling:
`sensitive: true` controls how a value is **displayed and stored** afterward (masking, redaction) — the raw value is still extracted by the LLM and lands in session state during collection. For data that must **never** reach the model, transcript, session state, or logs at all — a full card number, CVV, and similar PCI-scoped values — use [`SECURE_INPUT`](/agent-platform/abl-reference/flow#secure_input) instead of `GATHER`. `SECURE_INPUT` is currently a **`FLOW:`-step-only** action: it has no reasoning-mode/top-level form, so it's only available to agents that have a `FLOW:` section — a pure reasoning-mode agent has no equivalent construct today.
```yaml theme={null}
GATHER:
ssn_last4:
prompt: "Last 4 digits of your Social Security Number?"
type: string
required: true
sensitive: true
sensitive_display: mask
mask_config:
showFirst: 0
showLast: 4
char: "*"
```
When the field name is non-canonical, add `pii_type` so the redactor can produce a shape-preserving
mask:
```yaml theme={null}
GATHER:
member_id:
prompt: "Your membership ID?"
type: string
sensitive: true
sensitive_display: mask
pii_type: custom # email | phone | ssn | credit_card | address | name | custom
```
Alternatively, bind the field to a **named project PII pattern** with `pii_pattern_ref` instead of `pii_type`:
```yaml theme={null}
GATHER:
loyalty_number:
prompt: "Your loyalty number?"
type: string
sensitive: true
sensitive_display: mask
pii_pattern_ref: loyalty_number_pattern
```
`pii_pattern_ref` is a free-form reference — the parser has no project scope, so it can't validate that the named pattern exists. It's resolved at runtime when the field is written; a reference that matches no configured pattern falls back to `pii_type` and emits `pii_pattern_ref_unresolved`. Studio also flags a dangling reference in the gather field editor.
### Sensitive display modes
| Mode | Description | Example output |
| - | - | - |
| `redact` | Replace entire value with placeholder | `[REDACTED]` |
| `mask` | Show partial value with masking characters | `****1234` |
| `replace` | Replace with a generic description | `[SSN]` |
### Mask configuration
| Property | Type | Default | Description |
| - | - | - | - |
| `showFirst` | `number` | `0` | Number of characters to show at the start |
| `showLast` | `number` | `4` | Number of characters to show at the end |
| `char` | `string` | `"*"` | Masking character |
### Transient fields
Fields marked `transient: true` are automatically cleared from the session after the gather phase completes. Use this for sensitive data that should not persist beyond its immediate use:
```yaml theme={null}
GATHER:
card_number:
prompt: "Card number for verification?"
type: string
required: true
sensitive: true
transient: true
```
## Extraction patterns
The `extraction_pattern` property defines a custom regex for extracting structured values from user input. Combined with `extraction_group`, it specifies which capture group to use:
```yaml theme={null}
GATHER:
routing_number:
prompt: "What is the routing number?"
type: string
required: true
extraction_pattern: "(?:routing|aba|transit).*?(\d{9})"
extraction_group: 1
```
| Property | Type | Default | Description |
| - | - | - | - |
| `extraction_pattern` | `string` | *none* | Regex pattern for value extraction |
| `extraction_group` | `number` | `0` | Capture group index (0 = full match) |
## Semantics
The `semantics` block provides supplemental metadata that helps the runtime interpret and process field values:
```yaml expandable=true theme={null}
GATHER:
address:
prompt: "What is the delivery address?"
type: string
required: true
semantics:
format: "US postal address"
components: [street, city, state, zip]
transfer_amount:
prompt: "How much to transfer?"
type: number
required: true
semantics:
unit: "USD"
convert_to: "cents"
```
### Semantics properties
| Property | Type | Description |
| - | - | - |
| `format` | `string` | Expected format description |
| `components` | `string[]` | Sub-components of the value |
| `unit` | `string` | Unit of measurement |
| `lookup` | `string` | Lookup table reference for validation |
| `convert_to` | `string` | Target unit for conversion |
| `locale` | `string` | Locale for parsing |
| `temporal` | `string` | Temporal direction for date/datetime fields: `future`, `past`, or `any` (default). `future` biases to the next occurrence and rejects past dates; `past` rejects future dates. |
| `enum_set` | `string[]` | Allowed enumeration values (alternative to top-level `options`). |
| `kore_entity_type` | `string` | Kore entity type hint used for recognition. |
## Complete example
```yaml expandable=true theme={null}
GATHER:
source_account:
prompt: "Which account would you like to send from?"
type: string
required: true
transfer_type:
prompt: "Is this a domestic or international wire?"
type: string
required: true
validate: enum(domestic, international)
infer: true
beneficiary_name:
prompt: "Full legal name on the beneficiary's account?"
type: string
required: true
amount:
prompt: "How much would you like to send?"
type: number
required: true
validate: min(1)
currency:
prompt: "What currency? (for example. USD, EUR, GBP)"
type: string
required: true
default: "USD"
infer: true
purpose_code:
prompt: "Purpose of this transfer?"
type: string
required: true
infer: true
reference:
prompt: "Reference or memo for the beneficiary? (optional)"
type: string
required: false
```
***
**Related articles:**
* [Tools](/agent-platform/abl-reference/tools) -- tool definitions (tools and gather work together)
* [FLOW](/agent-platform/abl-reference/flow) -- GATHER within flow steps uses a different syntax
* [Language overview](/agent-platform/abl-reference/language-overview) -- syntax rules for lists and blocks
# Guardrails
Source: https://koreai.mintlify.app/agent-platform/abl/reference/guardrails
Guardrails are safety checks that evaluate agent inputs and outputs to detect harmful, non-compliant, or malformed content. Unlike [constraints](/agent-platform/abl-reference/memory-and-constraints#constraints) (which enforce business rules), guardrails protect against safety and quality violations at the content level. The `GUARDRAILS:` block defines named guardrail rules.
## Overview
ABL guardrails use a three-tier evaluation model:
1. **CEL-based** (Tier 1) -- fast, deterministic expression checks.
2. **Model-based** (Tier 2) -- pre-trained safety classification models (for example, OpenAI moderation).
3. **LLM-based** (Tier 3) -- natural language checks evaluated by an LLM.
Each guardrail specifies an application point (when to check), a check expression or prompt, and an action to take when the check fails.
```yaml theme={null}
GUARDRAILS:
profanity_filter:
kind: input
check: not_contains_blocked_words(input)
action: block
message: "Your message was blocked. Please keep the conversation respectful."
priority: 1
pii_output_prevention:
kind: output
check: not_contains_ssn(output)
action: redact
message: "Sensitive information has been redacted."
priority: 0
```
## Application points
The `kind` property determines when the guardrail is evaluated during the agent's processing pipeline.
| Kind | Evaluation point |
| - | - |
| `input` | Before the user's message reaches the LLM. |
| `output` | After the LLM generates a response, before it is sent to the user. |
| `both` | Evaluated on both input and output. |
| `tool_input` | Before parameters are sent to a tool call. |
| `tool_output` | After a tool returns its result, before the result enters the LLM context. |
| `handoff` | Before context is passed to another agent during a handoff. |
## Guardrail properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | `string` | Yes | -- | Unique identifier for the guardrail (the YAML key). |
| `kind` | `string` | Yes | -- | Application point. See [Application points](#application-points). |
| `check` | `string` | No | -- | CEL expression to evaluate (Tier 1). Omit for model-based or LLM-based. |
| `action` | `string` | Yes | -- | Action when the check fails. See [Actions](#actions). |
| `message` | `string` | No | -- | Human-readable message displayed or logged when the guardrail triggers. |
| `priority` | `number` | No | `100` | Evaluation priority. Lower values are evaluated first. |
| `provider` | `string` | No | -- | Model provider name for Tier 2 checks (for example. `openai_moderation`). |
| `category` | `string` | No | -- | Safety taxonomy category for Tier 2 (for example. `hate`, `violence`). |
| `threshold` | `number` | No | -- | Score threshold (0.0--1.0) for model-based checks. |
| `llm_check` | `string` | No | -- | Natural language prompt for Tier 3 LLM-based checks. |
| `severity_actions` | `object` | No | -- | Per-severity action overrides. See [Graduated actions](#graduated-actions). |
| `fix_strategy` | `string` | No | -- | Fix strategy when `action: fix`. See [Fix strategies](#fix-strategies). |
| `fix_expression` | `string` | No | -- | CEL expression for the `custom` fix strategy. |
| `max_reasks` | `number` | No | `2` | Maximum reask attempts when `action: reask`. |
| `filter_min_length` | `number` | No | -- | Minimum content length after filtering. Below this threshold, block instead. |
| `streaming` | `boolean` | No | `false` | Enable mid-stream evaluation for streaming responses. |
| `streaming_interval` | `string` | No | -- | Streaming evaluation granularity. See [Streaming evaluation](#streaming-evaluation). |
## Actions
The `action` property determines the runtime behavior when a guardrail check fails.
| Action | Behavior |
| - | - |
| `block` | Reject the content entirely. For input, the user message is discarded. For output, the response is withheld. |
| `warn` | Allow the content through but emit a warning event. The `message` is logged, not sent to the user. |
| `redact` | Replace the offending content with a redaction marker and continue. The sanitized content is passed through. |
| `escalate` | Trigger human escalation for review. The content is held pending human decision. |
| `fix` | Automatically repair the content using a fix strategy. See [Fix strategies](#fix-strategies). |
| `reask` | Reject the LLM output and re-prompt with the guardrail's message appended as additional guidance. |
| `filter` | Remove the offending portions while preserving the rest of the content. |
## Three-tier implementation
### Tier 1: CEL-based checks
CEL (Common Expression Language) checks are fast, deterministic rules evaluated without calling an external model. Use the `check` property with a CEL expression.
```yaml theme={null}
GUARDRAILS:
length_limit:
kind: output
check: length(output) < 10000
action: warn
message: "Response exceeds recommended length."
ssn_detection:
kind: input
check: not_matches_pattern(input, "\\b\\d{3}-\\d{2}-\\d{4}\\b")
action: redact
message: "SSN detected and redacted."
```
#### Content, session-state, and tool-result variables
Every `check` is evaluated with a content variable already bound to the kind's own name — `input`, `output`, `tool_input`, `tool_output`, or `handoff` — and **`text` is always bound too, to that same checked content, regardless of `kind`**. Prefer `text` for a check you intend to reuse across kinds.
`content` is an alias of `text`, but it (along with two more builtins, `session_state` — a masked view of session values — and `tool_results` — this turn's bounded tool results) is bound only when the agent sets `EXECUTION: guardrail_builtins: true`:
```yaml theme={null}
EXECUTION:
guardrail_builtins: true
GUARDRAILS:
no_leak:
kind: output
check: not_contains_pii(content)
action: block
```
Without `guardrail_builtins: true`, a check that reads `content`, `session_state`, or `tool_results` is unbound: it errors at evaluation time and the content passes through unchecked (fails open). The compiler warns `GUARDRAIL_BUILTIN_NOT_BOUND` for this — write `text` instead, or opt in. And if the agent separately declares a session variable literally named `content`, turning the opt-in on makes `content` resolve to the checked text instead of that session variable — the compiler warns `GUARDRAIL_CONTENT_SHADOWS_SESSION_VAR`; rename the session variable, or use `text` for the checked text.
### Tier 2: Model-based checks
Model-based checks use a pre-trained classification model to score content. You specify a `provider`, an optional `category`, and a `threshold`.
```yaml theme={null}
GUARDRAILS:
toxicity_detection:
kind: input
provider: openai_moderation
category: hate
threshold: 0.7
action: block
message: "Content flagged for hateful language."
```
### Tier 3: LLM-based checks
LLM-based checks use a natural language prompt evaluated by an LLM. Use the `llm_check` property with a descriptive prompt.
```yaml theme={null}
GUARDRAILS:
medical_advice_check:
kind: output
llm_check: "Does this response provide specific medical diagnoses or prescribe medication? Answer YES or NO."
action: block
message: "I'm not able to provide medical diagnoses. Please consult a healthcare professional."
```
## Fix strategies
When `action: fix`, the `fix_strategy` property determines how content is repaired.
| Strategy | Behavior |
| - | - |
| `truncate` | Truncate content to the maximum allowed length. |
| `strip_html` | Remove HTML tags from the content. |
| `redact_pii` | Detect and replace PII patterns with redaction markers. |
| `normalize` | Normalize whitespace, encoding, and special characters. |
| `custom` | Apply a custom CEL expression defined in `fix_expression`. |
### Example: fix with truncation
```yaml theme={null}
GUARDRAILS:
response_length:
kind: output
check: length(response) <= 5000
action: fix
fix_strategy: truncate
message: "Response was trimmed to fit the maximum length."
```
### Example: custom fix expression
```yaml theme={null}
GUARDRAILS:
normalize_whitespace:
kind: output
check: not_contains_excessive_whitespace(response)
action: fix
fix_strategy: custom
fix_expression: "collapse_whitespace(response)"
```
## Graduated actions
Use `severity_actions` to apply different actions based on the severity of the violation. The keys are severity labels and the values are action names.
```yaml theme={null}
GUARDRAILS:
content_safety:
kind: output
provider: openai_moderation
threshold: 0.5
action: warn
severity_actions:
low: warn
medium: reask
high: block
message: "Content flagged by safety model."
```
## Streaming evaluation
For streaming responses, guardrails can evaluate content as it is generated rather than waiting for the complete response.
| Property | Values | Description |
| - | - | - |
| `streaming` | `true`, `false` | Enable mid-stream evaluation. |
| `streaming_interval` | `token`, `sentence`, `chunk_size` | Granularity of streaming evaluation. |
```yaml theme={null}
GUARDRAILS:
realtime_safety:
kind: output
provider: openai_moderation
threshold: 0.8
action: block
streaming: true
streaming_interval: sentence
message: "Response generation halted due to safety concern."
```
When a streaming guardrail triggers, the response generation is halted at the current point and the `message` is sent to the user.
## Reask behavior
When `action: reask`, the runtime rejects the LLM output, appends the guardrail's `message` as additional guidance, and re-prompts. The `max_reasks` property controls how many times this can happen before falling back to a block.
```yaml theme={null}
GUARDRAILS:
factual_grounding:
kind: output
llm_check: "Does this response make claims not supported by the provided context?"
action: reask
max_reasks: 3
message: "Stick to information from the provided context. Do not make unsupported claims."
```
## Priority and evaluation order
Guardrails are evaluated in order of `priority` (lower values first). When multiple guardrails have the same priority, they are evaluated in declaration order.
A `block` action from any guardrail stops further evaluation. `warn` actions do not stop evaluation; all subsequent guardrails continue to run.
## Built-in guardrail templates
ABL ships a set of built-in, CEL-based (Tier 1) guardrail templates focused on prompt-injection and
secret-leak protection:
| Template | Kind | Detects | Action |
| - | - | - | - |
| `detect_instruction_override` | input | Attempts to override or ignore system instructions | `warn` |
| `detect_role_manipulation` | input | Attempts to manipulate the AI's role or persona | `warn` |
| `detect_system_prompt_extraction` | input | Attempts to extract the system prompt | `warn` |
| `detect_encoding_tricks` | input | Encoding-based obfuscation (base64, rot13, hex) | `warn` |
| `detect_credential_leak` | output | Leaked credentials, API keys, or tokens in output | `redact` |
You can also author your own guardrails for domain-specific concerns (account-number masking, SSN
redaction, profanity, etc.) using the tiers described above.
## Complete example
```yaml expandable=true theme={null}
GUARDRAILS:
account_number_masking:
kind: output
check: not_contains_full_account_number(response)
action: redact
message: "Account numbers are masked. Only the last 4 digits are displayed."
priority: 0
credential_input:
kind: input
check: not_contains_credentials(input)
action: redact
message: "Please never share passwords or PINs in this chat."
priority: 0
credit_card_detection:
kind: input
check: not_matches_pattern(input, "\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b")
action: redact
message: "Credit card number redacted for your security."
toxicity_check:
kind: output
check: toxicity_score(response) < 0.5
action: block
message: "Response blocked due to potential harmful content."
priority: 1
```
***
**Related articles:**
* [Memory & Constraints](/agent-platform/abl-reference/memory-and-constraints#constraints) -- business rule enforcement (distinct from content safety)
* [Expressions & functions](/agent-platform/abl-reference/expressions-and-functions) -- CEL expression syntax for `check` properties
* [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) -- ESCALATE action for human review
# ABL overview
Source: https://koreai.mintlify.app/agent-platform/abl/reference/language-overview
Agent Blueprint Language (ABL) is the enterprise control plane for agentic AI — a schema-driven language purpose-built for multi-agent orchestration where deterministic governance meets autonomous reasoning. ABL spans the full control spectrum: delegate autonomously, supervise selectively, or lock down as a deterministic state machine. Agent definitions compile into immutable artifacts, and AI can author blueprints just as humans do.
## Design principles
ABL agents are goal-driven rather than script-driven. Rather than encoding an explicit numbered
state machine, you declare the outcome the agent should achieve and the rules it must respect, and
the runtime reasons toward that goal — falling back to deterministic execution wherever you need it.
1. **Goal-oriented** — agents work toward a declared `GOAL:`, not through hard-coded scripts.
2. **Constraint-guarded** — business rules are enforced by `CONSTRAINTS:` and `GUARDRAILS:`, independent of step order.
3. **Memory-enabled** — agents can remember user and session state across turns and sessions.
4. **Composable** — agents can `DELEGATE:` to sub-agents or `HANDOFF:` to peers and supervisors.
5. **Human-in-the-loop** — `ESCALATE:` provides explicit paths to human operators.
Reasoning and determinism are not either/or. An agent reasons by default; adding a `FLOW:` section
lets you make any part of the conversation deterministic, step by step (see [Auto-detection](#auto-detection)).
## File structure
An ABL document is a plain-text file composed of top-level **sections**, each introduced by an uppercase keyword followed by a colon. Sections can appear in any order, but by convention the `AGENT:` declaration comes first.
```yaml expandable=true theme={null}
AGENT: Customer_Support
GOAL: |
Help customers resolve billing questions.
PERSONA: |
Friendly, patient support representative.
TOOLS:
lookup_account(account_id: string) -> {name: string, balance: number}
description: "Retrieve account details"
type: http
endpoint: "/api/accounts/lookup"
method: POST
GATHER:
account_id:
prompt: "What is your account number?"
type: string
required: true
```
### File extensions
| Extension | Contents |
| - | - |
| `.agent.abl` | Agent definition (most common) |
| `.tools.abl` | Reusable tool library (see [Tool file imports](/agent-platform/abl-reference/tools#tool-file-imports)) |
| `.agent.yaml` | Agent definition in YAML format |
### Required sections
Every agent document must contain:
* `AGENT:` -- the agent name
* `GOAL:` -- the agent's objective
All other sections are optional. If a section is omitted, its value defaults to empty (empty list, empty object, or platform defaults as specified in each section's reference).
### Recognized top-level sections
| Section | Purpose | Reference |
| - | - | - |
| `AGENT:` | Agent name declaration | [Agent declaration](/agent-platform/abl-reference/agent-declaration) |
| `VERSION:` | Semantic version | [Agent declaration](/agent-platform/abl-reference/agent-declaration#version) |
| `DESCRIPTION:` | Human-readable description | [Agent declaration](/agent-platform/abl-reference/agent-declaration#description) |
| `LANGUAGE:` | Agent language code | [Agent declaration](/agent-platform/abl-reference/agent-declaration#language) |
| `GOAL:` | Agent objective | [Agent declaration](/agent-platform/abl-reference/agent-declaration#goal) |
| `PERSONA:` | Agent personality description | [Agent declaration](/agent-platform/abl-reference/agent-declaration#persona) |
| `LIMITATIONS:` | Explicit boundaries | [Agent declaration](/agent-platform/abl-reference/agent-declaration#limitations) |
| `IDENTITY:` | Combined identity block | [Agent declaration](/agent-platform/abl-reference/agent-declaration#identity) |
| `INSTRUCTIONS:` | Operational instructions | [Agent declaration](/agent-platform/abl-reference/agent-declaration#instructions) |
| `EXECUTION:` | Model and runtime configuration | [Agent declaration](/agent-platform/abl-reference/agent-declaration#execution-configuration) |
| `TOOLS:` | Tool definitions | [Tools](/agent-platform/abl-reference/tools) |
| `GATHER:` | Information collection fields | [GATHER](/agent-platform/abl-reference/gather) |
| `FLOW:` | Structured execution steps | [FLOW](/agent-platform/abl-reference/flow) |
| `MEMORY:` | Session and persistent state | [Memory & Constraints](/agent-platform/abl-reference/memory-and-constraints) |
| `CONSTRAINTS:` | Business rule enforcement | [Memory & Constraints](/agent-platform/abl-reference/memory-and-constraints#constraints) |
| `GUARDRAILS:` | Input/output safety checks | [Guardrails](/agent-platform/abl-reference/guardrails) |
| `DELEGATE:` | Sub-agent delegation | [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#delegate) |
| `HANDOFF:` | Agent transfer | [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#handoff) |
| `ESCALATE:` | Human escalation | [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#escalate) |
| `COMPLETE:` | Completion conditions | [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#complete) |
| `ON_ERROR:` | Error handlers | [Lifecycle & Hooks](/agent-platform/abl-reference/lifecycle-and-hooks#on_error-handlers) |
| `ON_START:` | Session initialization | [Lifecycle & Hooks](/agent-platform/abl-reference/lifecycle-and-hooks#on_start-handler) |
| `MESSAGES:` | Customizable system messages | [Lifecycle & Hooks](/agent-platform/abl-reference/lifecycle-and-hooks#messages) |
| `TEMPLATES:` | Reusable response templates | [Rich content](/agent-platform/abl-reference/rich-content) |
| `HOOKS:` | Lifecycle event handlers | [Lifecycle & Hooks](/agent-platform/abl-reference/lifecycle-and-hooks#hooks) |
| `NLU:` | Natural language understanding | [NLU](/agent-platform/abl-reference/nlu) |
| `MULTI_INTENT:` | Multi-intent configuration | [NLU](/agent-platform/abl-reference/nlu#multi-intent-handling-multi_intent) |
| `INTENTS:` | Intent categories (routing) | [NLU](/agent-platform/abl-reference/nlu#intent-categories-intents-section) |
| `ENTITIES:` | Reusable named entities | [NLU](/agent-platform/abl-reference/nlu#named-entities-entities-section) |
| `LOOKUP_TABLES:` | Reference data for validation | [Data Types & Utilities](/agent-platform/abl-reference/data-types-and-utilities#lookup-tables) |
| `SYSTEM_PROMPT:` | Custom system prompt template | [Agent declaration](/agent-platform/abl-reference/agent-declaration#system_prompt) |
| `ATTACHMENTS:` | File/media collection | [Data Types & Utilities](/agent-platform/abl-reference/data-types-and-utilities#attachments) |
| `DESTINATIONS:` | Outbound HTTP delivery targets | [Data Types & Utilities](/agent-platform/abl-reference/data-types-and-utilities#destinations) |
| `TRANSFER_FLOW_POLICIES:` | Named policies referenced by escalation routing | [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#transfer-flow-policies) |
| `RETURN_HANDLERS:` | Named parent-resume handlers for returnable handoffs | [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor#return-handlers) |
| `ACTION_HANDLERS:` | Reusable interactive-action handlers | [Rich Content & Expressions](/agent-platform/abl-reference/rich-content#reusable-handlers-action_handlers) |
| `CONVERSATION:` | Conversation-behavior configuration | [Behavior profiles](/agent-platform/abl-reference/behavior-profiles#conversation) |
`TESTS:` is a reserved section name the parser accepts but currently **ignores** — content under
it is parsed and discarded with no effect. It exists to avoid "unknown section" errors; do not
rely on it for behavior.
## Syntax rules
### Indentation
ABL uses indentation to express nesting. Use **spaces** (two or more) for indentation. Tabs are accepted but spaces are preferred for consistency. Content nested under a section keyword must be indented further than the keyword line.
```yaml theme={null}
TOOLS:
search(query: string) -> {results: string[]}
description: "Search the catalog"
type: http
endpoint: "/api/search"
method: GET
```
### Keywords
Section keywords are followed by a colon (`:`). In the `.agent.abl` format, section keywords **must be uppercase** (`AGENT:`, `GOAL:`, `TOOLS:`, …). In the `.agent.yaml` format, they **must be lowercase** (`agent:`, `goal:`, …). Mixed case is not supported, and the two formats do not accept each other's casing.
### Comments
Lines beginning with `#` are comments and are ignored by the parser. Comments can appear anywhere in the file.
```yaml theme={null}
# This is a comment
AGENT: My_Agent
GOAL: "Help with orders" # Inline comments are NOT supported in ABL format
```
### Strings
Strings can be written in several forms:
| Form | Syntax | Use case |
| - | - | - |
| Quoted | `"value"` | Single-line values |
| Unquoted | `value` | Simple values without special characters |
| Pipe block | \| followed by indented lines | Multi-line text (preserves newlines) |
```yaml theme={null}
GOAL: "Single line goal"
GOAL: |
Multi-line goal that preserves
line breaks within the block.
LANGUAGE: "en"
```
### Lists
Lists use YAML-style `- item` syntax with indentation:
```yaml theme={null}
LIMITATIONS:
- "Cannot access external systems"
- "Cannot process payments directly"
```
## Auto-detection
The parser determines the document type from the first meaningful keyword it encounters:
| First keyword | Document type |
| - | - |
| `AGENT:` | Agent document |
| `SUPERVISOR:` | Supervisor document |
| `BEHAVIOR_PROFILE:` | Behavior profile document |
| `TOOLS:` (at root level in a `.tools.abl` file) | Tool file document |
Within an agent document, execution mode is **not** declared via a global `MODE:` keyword. Instead, agents operate in reasoning mode by default -- the LLM decides which tools to call and when, guided by the `GOAL:` and `PERSONA:`.
Any agent can optionally include a `FLOW:` section to add structured execution steps. Adding a `FLOW:` section gives the agent a step-by-step execution graph. Each step within the flow declares `REASONING: true` or `REASONING: false` to control whether that individual step uses LLM reasoning or deterministic execution.
The `MODE:` keyword is deprecated and produces a parser error if used. Remove it and use per-step `REASONING:` declarations within `FLOW:` instead.
## YAML format
ABL supports an alternative YAML format for agent definitions. YAML files use `.agent.yaml` as their extension. The parser auto-detects YAML format by checking whether the first non-comment, non-empty lines use lowercase keys matching known ABL sections (`agent`, `goal`, `persona`, `tools`, etc.).
```yaml expandable=true theme={null}
agent: Customer_Support
goal: |
Help customers resolve billing questions.
persona: |
Friendly, patient support representative.
tools:
- name: lookup_account
parameters:
- name: account_id
type: string
returns:
type: object
fields:
name: string
balance: number
description: Retrieve account details
type: http
endpoint: /api/accounts/lookup
method: POST
```
Both formats produce the same intermediate representation (AST) and compile to identical runtime artifacts.
## Version declaration
The optional `VERSION:` directive specifies the document version in semver format:
```yaml theme={null}
VERSION: "2.0.0"
```
When omitted, the version defaults to `"1.0.0"`. The version is stored in the document metadata and can be used for compatibility tracking.
## Compilation
ABL source files are compiled to a typed intermediate representation (**AgentIR**) at deploy time.
The runtime loads and executes the IR directly — there is no code-generation step.
```yaml theme={null}
ABL source (.agent.abl / .agent.yaml)
│
▼
Parser → parses the document into an AST
│
▼
Compiler → produces a CompilationOutput: a map of AgentIR
│ instances (one per agent, including supervisors)
│ plus the entry agent
▼
Runtime → loads the IR and executes it (reasoning loop,
flow steps, routing, constraint checking)
```
Each agent (and supervisor) compiles to a single `AgentIR` that gathers the document's sections into
typed configuration — identity (from `GOAL`/`PERSONA`/`LIMITATIONS`, including a generated system
prompt), tools, gather, constraints and guardrails, coordination (handoffs/delegates/escalation),
completion, and memory. Supervisors are ordinary agents with routing populated, so a project's
agents and supervisors all live in one registry and can compose hierarchically.
## Template interpolation
Throughout ABL, string values support template interpolation using double-brace syntax:
```text theme={null}
"Hello, {{customer_name}}. Your balance is {{balance}}."
```
Conditional blocks use Handlebars-style helpers:
```text theme={null}
"{{#if exchange_rate}}Rate: {{exchange_rate}}{{/if}}"
```
Template expressions are resolved at runtime against session variables and tool results.
# Lifecycle and hooks
Source: https://koreai.mintlify.app/agent-platform/abl/reference/lifecycle-and-hooks
ABL provides lifecycle handlers and hooks that execute at specific points in an agent's execution cycle. These allow you to initialize state, run side effects, and handle errors without embedding that logic in the main conversation flow.
## Overview
| Handler | When it fires |
| - | - |
| `ON_START` | Once per session, before any user input. |
| `HOOKS` | At defined lifecycle points (before/after agent, turn). |
| `ON_ERROR` | When a specific error type occurs during execution. |
## ON\_START handler
The `ON_START` handler executes once when a new session initializes, before the agent processes any user input. Use it for greeting messages, initial tool calls, and variable initialization.
### Syntax
```yaml theme={null}
ON_START:
SET:
session_initialized = true
transfer_status = "pending"
retry_count = 0
CALL: check_returning_user()
RESPOND: |
Welcome to Wire Transfer Services. I can help you send
a domestic or international wire transfer.
Which account would you like to send from?
```
### ON\_START properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `SET` | `Record` | No | -- | Variable assignments executed at session start. |
| `CALL` | `string` | No | -- | Tool to call during initialization (supports `WITH:`/`AS:`). |
| `DELEGATE` | `string` | No | -- | Agent to delegate to during initialization. |
| `RESPOND` | `string` | No | -- | Greeting or welcome message sent to the user. |
| `MESSAGE_KEY` | `string` | No | -- | Locale catalog key used to localize the greeting. |
| `VOICE` | `object` | No | -- | Voice-specific overrides for the response. See [Rich content](/agent-platform/abl-reference/rich-content). |
| `RICH_CONTENT` | `object` | No | -- | Rich content format variants (the `.agent.abl` keyword is `FORMATS:`). See [Rich content](/agent-platform/abl-reference/rich-content). |
| `ACTIONS` | `object` | No | -- | Interactive actions attached to the response. |
| `BRANCHES` | `object[]` | No | -- | Conditional greeting branches (`IF`/`ELSE`). See [Conditional greeting](#conditional-greeting). |
### Template references in ON\_START
You can reference named templates in the RESPOND value:
```yaml theme={null}
ON_START:
RESPOND: TEMPLATE(welcome)
```
This renders the template named `welcome` from the agent's `TEMPLATES:` block.
### ON\_START with tool call
```yaml theme={null}
ON_START:
CALL: check_cut_off_times(transfer_type = "all")
SET:
sanctions_clear = false
fraud_score = 0
RESPOND: "Welcome. Let me check today's processing windows."
```
The tool call executes first, and its result is available in the session context when the RESPOND message is rendered.
### Guarding a CALL with WHEN
An `ON_START` or hook `CALL` can carry an indented `WHEN:` guard so it only runs when the condition holds:
```yaml theme={null}
ON_START:
CALL: find_member_by_ani
WITH:
callerAni: ani_national
WHEN: ani_national != ""
HOOKS:
before_agent:
CALL: load_profile
WHEN: member_id != ""
```
The `WHEN:` must be indented **under** `CALL:`, not written as its sibling at the same indent. A sibling `WHEN:` is not read — the call runs unconditionally, and the parser warns `W496`.
### Conditional greeting
Use a `BRANCHES:` block to vary the greeting based on session context. Each branch is an `IF`
(or `CONDITION`) with a fallback `ELSE`, and can carry `RESPOND`, `MESSAGE_KEY`, `VOICE`, rich
content, and `ACTIONS`.
```yaml theme={null}
ON_START:
CALL: check_returning_user()
BRANCHES:
- IF: user.is_returning == true
RESPOND: "Welcome back, {{user.name}}!"
- ELSE:
RESPOND: "Welcome! How can I help you today?"
```
### ON\_START language resolution
A reasoning agent with no FLOW can resolve the caller's language from a startup `SET`/`CALL` and apply it before the welcome renders:
```yaml theme={null}
AGENT: Welcome_Agent
GOAL: "Help the caller"
TOOLS:
resolve_runtime_context_v1() -> object
description: "Resolve the caller language and greeting"
ON_START:
CALL: resolve_runtime_context_v1
AS: runtime_context
SET_INTERACTION_LANGUAGE: "{{runtime_context.spoken_language}}"
RESPOND: "{{runtime_context.greeting_text}}"
```
The tool needs an execution binding in the project. Top-level language directives resolve after top-level `SET`/`CALL` and before branch selection; a `BRANCHES` `DO:` list (below) can set the language too. On **Gemini Live** specifically, `ON_START` already runs before the initial provider setup, so the first connection uses this language in legacy, `runtime_guarded`, and `runtime_owned` orchestration modes — the channel must have the language configured in its own supported-language policy, or an unresolved/unsupported value is rejected without changing the policy default.
### Start-time HANDOFF
A `BRANCHES:` entry (or a bare top-level `HANDOFF:`) can hand the session to another declared local agent at start, before the first customer message is even understood. A branch's `DO:` list runs these actions in order, by default *after* the welcome renders:
| Action | Effect |
| - | - |
| `SET:` | Assigns session values. |
| `SET_INTERACTION_LANGUAGE:` / `CLEAR_INTERACTION_LANGUAGE:` | Sets or clears the voice language, same as elsewhere. |
| `CALL:` | Calls a tool (`WITH:`/`AS:`, as in a FLOW step `DO:`). A failed call stops the list. |
| `HANDOFF: ` | Hands off through the normal handoff lifecycle — the target's declared `WHEN`, `RETURN`, `CONTEXT`, `EXPERIENCE_MODE`, `ON_RETURN`, and `ON_FAILURE` all apply as usual. At most one handoff per list. |
If the list sets the interaction language, the prefix through the last language directive (before any `HANDOFF`) runs *before* the welcome renders instead, and the rest after:
```yaml theme={null}
SUPERVISOR: Router
GOAL: "Route customers by the page they came from"
TOOLS:
get_welcome(page: string) -> object
description: "The welcome for the page the customer came from"
HANDOFF:
- TO: Product_Search
RETURN: true
CONTEXT:
pass: [page_name, welcome]
- TO: Order_Agent
RETURN: false
ON_START:
CALL: get_welcome
WITH:
page: page_name
AS: welcome
BRANCHES:
- IF: page_name == "beauty_hub_screen"
RESPOND: "{{welcome.text}}"
DO:
- SET: entry_source = "app"
- HANDOFF: Product_Search
- ELSE:
RESPOND: "Hi, how can I help?"
```
A top-level `HANDOFF: ` (bare, bulleted, or the sole entry of a top-level `DO:`) runs only when **no branch matched** — a matched branch with no `DO:` list never falls back to it. With no branches at all, a bare `HANDOFF:` is instead decided by that declared route's own `WHEN` (and `ENABLED_WHEN`), checked once at session start:
```yaml theme={null}
SUPERVISOR: Router
GOAL: "Send customers who came from the sales page to Sales"
HANDOFF:
- TO: Sales
WHEN: page_name == "sales"
RETURN: false
ON_START:
RESPOND: "Welcome."
HANDOFF: Sales
```
`DELEGATE` and any other action in an `ON_START` list is dropped with a warning (`W380`-`W388`) — it is never run from here. A handoff target that isn't a declared local `HANDOFF` entry is a compile error (`ON_START_HANDOFF_TARGET_INVALID`). The start-time handoff is local only: a route `WHEN` that only a model could judge (natural language, `intent.*`) is never satisfied at start, and a remote (A2A) target is always refused.
| Diagnostic | Severity | Catches |
| - | - | - |
| `ON_START_HANDOFF_ACTIVATED` / `ON_START_BRANCH_DO_ACTIVATED` | warning (informational) | Confirms a top-level `HANDOFF`, or a branch's `DO:` list, really does run at session start — a reminder, not a mistake. |
| `ON_START_HANDOFF_UNREACHABLE` | warning | A top-level `HANDOFF:` is dead code because an `ELSE` branch always matches first, so the fallback handoff can never run. |
| `ON_START_DELEGATE_WITH_HANDOFF` | warning | `ON_START` configures both a top-level `DELEGATE:` and a handoff — when the handoff runs, the child owns the session and the delegate is never returned. Keep one or the other. |
**How it runs, by start type:**
* **Eager start** (no customer message yet — Web SDK/web-debug connect, pipeline voice answer, an identified agent-assist welcome): the welcome is delivered, then the child starts on its own with no synthetic customer message and speaks its own opener. Order: entry agent's `before_agent` output, the welcome, the handoff announcement (if the experience mode speaks one), the child's `before_agent` output, the child's own first output. A `RETURN: true` child stays active after that unanswered opener; ordinary return rules apply from the customer's first real message on.
* **Lazy start** (the first customer message starts the session — HTTP chat, LiveKit, SDK `initialMessage`): the welcome is **not** delivered, and that first message is forwarded to the child once — after the entry agent's input guardrails/constraints (and, for a FLOW entry, its `before_turn` hook) — taking precedence over the entry agent's own first-turn routing.
* **Realtime speech-to-speech** sessions run the list's `SET`/`CALL` but never the handoff itself; the entry agent keeps the session.
Every outcome is recorded in the `dsl_on_start_handoff` trace (`executed`, `blocked`, `failed`, `call_failed`, `superseded_by_tool_hook`, `skipped_mode`) with a reason and identifiers only — never customer data. When the handoff doesn't run: a route `WHEN` simply not met shows nothing, not even `ON_FAILURE` text; a refused or failed handoff (a handoff guardrail, an unloaded target) shows only what the route's `ON_FAILURE` authors (`RESPOND` shows its text; `CONTINUE`, or no `ON_FAILURE`, shows nothing — never a generic failure message) — except `ESCALATE`, which escalates the session instead of keeping it with the entry agent; a target an `ENABLED_WHEN` switched off follows the normal authored-route rule (use `ON_FAILURE: CONTINUE` to keep the start silent).
After the start, the route behaves as an ordinary handoff — `RETURN`, `CONTEXT`, and `ON_RETURN` apply as usual — and the child never runs its own `ON_START` (which fires once per session, for the entry agent only). A route `WHEN` that reads start data stays true for the rest of the session, so a later turn can still route back to that agent after it returns. If the handoff should really happen only at start, put the condition on a branch `IF:` and leave the route's own `WHEN` unset, or clear the triggering value on return. Pairing with `ENABLED_WHEN` cleared on return (`ENABLED_WHEN: promo_open == true` with `ON_RETURN: set: promo_open = false`) also stops the model from choosing that agent on later turns.
Caller data asserted by the browser (public-key SDK init) is **not verified**. When a start-time routing decision matters, mint the caller data on your own server through the customer bootstrap endpoint rather than trusting a client-asserted value in a branch `IF:` or route `WHEN`.
## HOOKS
The `HOOKS:` block defines actions that run at four lifecycle points. Unlike `ON_START`, hooks fire repeatedly throughout the session.
### Syntax
```yaml expandable=true theme={null}
HOOKS:
before_agent:
SET:
agent_start_time = NOW()
CALL: load_user_preferences()
after_agent:
CALL: save_session_summary()
before_turn:
SET:
turn_start_time = NOW()
after_turn:
CALL: log_audit_event()
SET:
turn_count = ADD(turn_count, 1)
```
### Hook points
| Hook | When it fires |
| - | - |
| `before_agent` | Once when the agent is first activated (before it processes its first message). |
| `after_agent` | Once when the agent completes or is deactivated. |
| `before_turn` | Before each user message is processed. |
| `after_turn` | After each turn completes (after the agent's response is sent). |
### Hook action properties
Each hook supports the same set of action properties:
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `SET` | `Record` | No | -- | Variable assignments. |
| `CALL` | `string` | No | -- | Tool to call (supports `WITH:`/`AS:`). |
| `RESPOND` | `string` | No | -- | Message to send (uncommon in hooks, but supported). |
| `VOICE` | `object` | No | -- | Voice-specific overrides. |
| `RICH_CONTENT` | `object` | No | -- | Rich content format variants. |
| `ACTIONS` | `object` | No | -- | Interactive actions. |
| `SET_INTERACTION_LANGUAGE` | `string` | No | -- | Set the authoritative session interaction language — a literal BCP-47 tag (for example `es`, `es-ES`, `fr-CA`) or a dynamic value that resolves to one at runtime. Subsequent actions and turns use it. |
| `CLEAR_INTERACTION_LANGUAGE` | `boolean` | No | -- | Remove the interaction-language override, reverting to automatic detection. Takes no value. |
| `critical` | `boolean` | No | `false` | If `true`, a hook failure aborts the turn; otherwise the failure is logged and execution continues. |
`SET_INTERACTION_LANGUAGE` and `CLEAR_INTERACTION_LANGUAGE` are action-list actions — like `SET` and `CLEAR`, they can appear in any action list (`ON_START`, `HOOKS`, `ON_ACTION` handlers). Use them to pin the language the agent responds in — for example, after detecting a caller's preferred language or reading it from a profile.
The value is classified when the agent is compiled:
1. **Literal tag** — a value that canonicalizes as BCP-47 (`es`, `fr-CA`, …) is validated and fixed at compile time.
2. **Dynamic value** — anything else that looks like a session-variable path (`user.preferred_language`), a `{{template}}`, or a `${dotted.path}` expression is resolved at runtime, using the same value resolution as an ordinary `SET`.
3. Anything that is neither a valid literal tag nor a recognized dynamic form is a compile error.
```yaml theme={null}
HOOKS:
before_turn:
# Dynamic — resolved from session state each time the hook runs
- SET_INTERACTION_LANGUAGE: "{{user.preferred_language}}"
# Equivalent dotted-path and ${...} forms are also accepted:
# - SET_INTERACTION_LANGUAGE: user.preferred_language
# - SET_INTERACTION_LANGUAGE: "${user.preferred_language}"
ON_START:
# Literal — validated once at compile time
- SET_INTERACTION_LANGUAGE: "es-MX"
```
A dynamic value is resolved and re-validated at **runtime**: if it doesn't resolve to a canonical BCP-47 tag, or resolves to a language the voice channel isn't configured to support, the override is not applied and the session keeps its current language.
### WHEN / RESPOND branches
A hook can respond conditionally: a `WHEN: ` with an indented `RESPOND: ""` is a **respond branch**. Branches are checked in declaration order, after the hook's own `CALL` and `SET` run, and the first branch whose condition holds wins:
```yaml theme={null}
HOOKS:
before_turn:
WHEN: email_verified && !zip_supplied
RESPOND: "Thanks. What is the ZIP code on the account?"
WHEN: account_locked == true
RESPOND: "This account is locked. I'll connect you with support."
```
* In a **reasoning agent's `before_turn` hook**, a matched branch's text becomes the turn's reply and the model is not called for that turn. Input guardrails and escalation triggers still run first and can still intervene.
* In **other hooks** (`before_agent`, `after_agent`, `after_turn`), a matched branch's text is sent as a message at that point in the lifecycle.
* A `WHEN` that can't be evaluated does not hold — evaluation falls through to the next branch (or to an unconditional `RESPOND`, if present).
* A branch can sit next to an unconditional `RESPOND:` in the same hook; the branches are the conditional alternative, evaluated independently.
A hook's `WHEN` must be valid CEL. An invalid `WHEN` (including old plain-English text left over from before this feature) is demoted to an unconditional `RESPOND` for backward compatibility, and the compiler warns `HOOK_WHEN_BRANCH_INVALID_CEL`. Fix the condition, or remove `WHEN:` if the response really is meant to run unconditionally.
### Example: audit logging hook
```yaml theme={null}
HOOKS:
after_turn:
CALL: log_wire_audit_event()
```
### Example: turn timing
```yaml theme={null}
HOOKS:
before_turn:
SET:
turn_start_time = NOW()
after_turn:
SET:
turn_duration_ms = SUB(NOW_MS(), turn_start_time_ms)
```
## ON\_ERROR handlers
The `ON_ERROR:` block defines how the agent responds to specific error types. Each handler matches an error type and specifies a response, optional retry logic, and a follow-up action.
### Syntax
```yaml expandable=true theme={null}
ON_ERROR:
tool_error:
RESPOND: "That system is responding slowly. Let me retry."
SUBTYPE: tool_timeout
RETRY: 2
THEN: CONTINUE
validation_error:
RESPOND: "That value doesn't look right. Could you double-check?"
THEN: CONTINUE
llm_error:
RESPOND: "I'm having trouble processing your request."
RETRY: 1
RETRY_BACKOFF: exponential
RETRY_MAX_DELAY: 10000
THEN: ESCALATE
```
### Error types
The error `type` (the handler's key) is matched against a **fixed, canonical set** — a handler whose
key is not one of these is flagged at compile time (`ON_ERROR_HANDLER_TYPE_DRIFT`) because it can
never match at runtime:
| Error type | When it occurs |
| - | - |
| `default` | Fallback handler used when no other type matches. |
| `llm_error` | The LLM call fails or errors. |
| `tool_error` | A tool call fails. Narrow it with `SUBTYPE`/`SUBTYPES` — known subtypes include `rate_limit`, `auth_failure`, `network_error`, and `tool_timeout`. |
| `routing_error` | Handoff/delegation/routing resolution fails. |
| `validation_error` | Gathered/extracted data fails validation. |
| `timeout_error` | A non-tool operation (for example the overall turn) times out. |
| `unknown_error` | An unexpected error occurred. |
Three names from older examples are **not** valid handler types and are silently rewritten as warnings, never matched at runtime: `tool_timeout` (use `tool_error` with `SUBTYPE: tool_timeout`), `invalid_input` (use `validation_error`), and `api_error` (use `tool_error` with an appropriate `SUBTYPE`, for example `network_error`).
Use `SUBTYPE` (single value) or `SUBTYPES` (array) to match more finely within a type — both keys are case-insensitive but conventionally written uppercase.
#### Inline shorthand
For a handler that only takes a terminal action, use the single-line form `error_name: ACTION`:
```yaml theme={null}
ON_ERROR:
unknown_error: ESCALATE
tool_error: HANDOFF Live_Agent
```
### Error handler properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `RESPOND` | `string` | No | -- | Message sent to the user when this error occurs. |
| `VOICE` | `object` | No | -- | Voice-specific overrides for the response. |
| `RICH_CONTENT` | `object` | No | -- | Rich content format variants. |
| `ACTIONS` | `object` | No | -- | Interactive actions. |
| `SET` | `Record` | No | -- | Variable assignments applied when this error is handled. |
| `RETRY` | `number` | No | -- | Number of retry attempts before executing `THEN`. |
| `RETRY_DELAY` | `number` | No | -- | Delay in milliseconds between retries. |
| `RETRY_BACKOFF` | `string` | No | -- | Backoff strategy for retries. See [Retry strategies](#retry-strategies). |
| `RETRY_MAX_DELAY` | `number` | No | -- | Maximum delay between retries in milliseconds. |
| `THEN` | `string` | No | -- | Action after retries are exhausted. See [Then actions](#then-actions). |
| `HANDOFF_TARGET` | `string` | No | -- | Explicit target agent for `THEN: HANDOFF` (alternative to `HANDOFF `). |
| `BACKTRACK_TO` | `string` | No | -- | Target step for `backtrack` action. |
| `SUBTYPE` / `SUBTYPES` | `string` / `string[]` | No | -- | Error subtype(s) for fine-grained matching within `tool_error` (for example `SUBTYPE: tool_timeout` or `SUBTYPES: [rate_limit, network_error]`). |
### Retry strategies
| Strategy | Behavior |
| - | - |
| `fixed` | Wait the same `RETRY_DELAY` between each attempt. |
| `exponential` | Double the delay after each attempt, up to `RETRY_MAX_DELAY`. |
| `linear` | Increase the delay by `RETRY_DELAY` after each attempt, up to `RETRY_MAX_DELAY`. |
### Then actions
| Action | Behavior |
| - | - |
| `CONTINUE` | Continue the conversation, skipping the failed operation. |
| `ESCALATE` | Trigger human escalation. |
| `HANDOFF` | Transfer to a specified agent (for example. `HANDOFF Live_Agent`, or via `HANDOFF_TARGET`). |
| `COMPLETE` | End the conversation. |
| `RETRY_STEP` | Re-run the current flow step from the beginning. |
| `BACKTRACK` | Return to a previous step specified by `BACKTRACK_TO`. |
### Step-level error handlers
Error handlers can be overridden at the step level within a flow. Step-level handlers take precedence over agent-level handlers for the same error type.
```yaml theme={null}
FLOW:
steps:
execute_payment:
CALL: process_payment
ON_ERROR:
- TYPE: tool_error
RESPOND: "Payment processing failed. Retrying..."
RETRY: 3
RETRY_BACKOFF: exponential
THEN: HANDOFF Payment_Support
```
#### Step-level error handler properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `TYPE` | `string` | Yes | -- | Error type to match. |
| `SUBTYPE` | `string` | No | -- | Error subtype for fine-grained matching (step-level handlers accept only the singular form). |
| `RESPOND` | `string` | No | -- | Message to the user. |
| `RETRY` | `number` | No | -- | Retry count. |
| `RETRY_DELAY` | `number` | No | -- | Delay between retries (ms). |
| `RETRY_BACKOFF` | `string` | No | -- | Backoff strategy. |
| `RETRY_MAX_DELAY` | `number` | No | -- | Maximum retry delay (ms). |
| `THEN` | `string` | No | -- | Action after retries are exhausted. |
| `HANDOFF_TARGET` | `string` | No | -- | Explicit target agent for `THEN: HANDOFF`. |
| `BACKTRACK_TO` | `string` | No | -- | Target step for backtrack. |
## Error handler resolution order
1. **Step-level handlers** are checked first, matching on error type and optional subtypes.
2. **Agent-level handlers** (`ON_ERROR:` block) are checked if no step-level handler matches.
3. If no handler matches, the runtime uses the default error message from the `MESSAGES:` block (`error_default`).
***
## MESSAGES
The top-level `MESSAGES:` block overrides the default system messages the runtime uses for errors,
constraint violations, gathering, handoffs, escalation, completion, and voice/telephony prompts. Any
key you don't set keeps its built-in default, and the templates support `{{}}` interpolation.
```yaml theme={null}
MESSAGES:
error_default: "Sorry, something went wrong. Please try again or type HELP."
constraint_blocked: "I'm not able to help with that request."
gather_prompt: "I need a few details: {{fields}}"
escalation_format: "Connecting you to a specialist. Reason: {{reason}}"
conversation_complete: "Thanks for chatting!"
```
### Common message keys
| Key | Default (abridged) | Used when… |
| - | - | - |
| `error_default` | "An error occurred. Please try again." | An error occurs and no `ON_ERROR` handler matches. |
| `constraint_blocked` | "I cannot proceed with that request." | A `CONSTRAINT` blocks the turn. |
| `gather_prompt` | "Please provide: " | Prompting for outstanding gather fields. |
| `escalation_format` | "Escalating to human agent. Reason: " | Formatting an escalation notice. |
| `conversation_complete` | "This conversation has been completed." | A conversation reaches `COMPLETE`. |
| `invalid_handoff` | "Unable to transfer to the requested agent." | A `HANDOFF` target can't be resolved. |
| `self_handoff` | "Cannot hand off to self." | An agent hands off to itself. |
| `tool_fallback_desc` | "Execute the requested operation." | A tool has no `description`. |
| `tool_confirmation_request` | "Please confirm before I go ahead. Shall I proceed?" | A tool needs the customer's approval. Supports `{{action_suffix}}` and `{{details}}`. See [Confirmation](/agent-platform/abl/reference/tools#what-the-customer-is-asked). |
| `tool_confirmation_duplicate_effect` | "I already completed this earlier in our conversation. Would you like me to do it again?" | A tool that already ran is about to run again. Supports `{{action_suffix}}`. |
`MESSAGES:` accepts **any** message key, not only the ones above — including error-subtype
messages (`error_tool_timeout`, `error_llm_error`, `error_validation`, …), multi-intent UX prompts
(`multi_intent_disambiguate_header`, `multi_intent_queued_notice`, …), handoff/routing messages
(`handoff_message`, `handoff_message_voice`, `routing_message`), voice prompts (`voice_repeat`,
`voice_nomatch`, `voice_noinput`, …), and the `greeting` / `out_of_scope` messages. Set only the
keys you want to override.
### Filler overrides (voice)
On a voice channel, the runtime speaks a short filler phrase (for example "One moment.") to avoid dead air while a tool call, reasoning step, handoff, delegation, extraction, or constraint check runs. `MESSAGES:` lets you override these with deterministic, author-controlled text using `filler_` keys:
```yaml theme={null}
MESSAGES:
filler_general: "One moment, please."
filler_tool_call: "Let me look that up for you."
filler_reasoning: "Let me think about that."
filler_handoff: "Connecting you now."
filler_delegation: "Checking with a specialist."
filler_extraction: "Let me pull those details."
filler_constraint_check: "Verifying that for you."
filler_tool_call_fr: "Je vérifie cela pour vous."
filler_tool_call_fr-CA: "Un instant, je regarde ça pour vous."
```
| Key | Spoken while… |
| - | - |
| `filler_general` | No more specific operation filler applies (the fallback). |
| `filler_tool_call` | A tool call is in flight. |
| `filler_reasoning` | A reasoning step is running. |
| `filler_handoff` | A `HANDOFF` is in progress. |
| `filler_delegation` | A `DELEGATE` is in progress. |
| `filler_extraction` | The agent is extracting/gathering structured data. |
| `filler_constraint_check` | A `CONSTRAINT` is being evaluated. |
Add a `_` suffix to supply a localized variant — either a bare BCP-47 primary subtag (`filler_tool_call_fr`) or a full region tag (`filler_tool_call_fr-CA`). At runtime the first match wins, checking the region tag before the bare subtag, and the operation-specific key before the general fallback:
`filler__` → `filler__` → `filler_` → `filler_general_` → `filler_general_` → `filler_general` → the platform's built-in localized fallback.
A key with a region tag (`filler_tool_call_fr-CA`) contains a hyphen — write it unquoted as a plain `key: value` line; both the `.agent.abl` and `.agent.yaml` formats parse the hyphen correctly.
Filler overrides are **static, deterministic text** — unlike other `MESSAGES:` keys, they do **not** support `{{}}` interpolation. User input is never substituted into a filler, by design.
***
**Related articles:**
* [Memory & Constraints](/agent-platform/abl-reference/memory-and-constraints) -- variables initialized in ON\_START and hooks
* [Multi-Agent & Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) -- ESCALATE and HANDOFF used in THEN actions
* [Rich content](/agent-platform/abl-reference/rich-content) -- voice and rich content in handler responses
# Memory and constraints
Source: https://koreai.mintlify.app/agent-platform/abl/reference/memory-and-constraints
This page documents two related ABL constructs:
* [`MEMORY:`](#memory) manages session and persistent state.
* [`CONSTRAINTS:`](#constraints) enforces business rules.
***
## MEMORY
Memory in ABL defines how an agent stores, retrieves, and persists information across conversation turns, sessions, and users. The `MEMORY:` block declares four categories: session variables, persistent variables, remember triggers, and recall instructions.
### Overview
Every agent has access to a memory configuration that controls data lifecycle:
* **Session variables** exist for the duration of a single conversation session.
* **Persistent variables** survive across sessions, scoped to a user or project.
* **Remember triggers** automatically store values when conditions are met.
* **Recall instructions** load stored facts at specific lifecycle events.
```yaml expandable=true theme={null}
MEMORY:
session:
- customer_id
- order_total
TYPE: number
DESCRIPTION: "Running total for the current order"
INITIAL: 0
RESET: per_session
persistent:
- user.preferred_language
- user.loyalty_tier
SCOPE: user
remember:
- WHEN: user_language IS SET
STORE: user_language -> user.language
TTL: "90d"
recall:
- ON: session:start
ACTION: inject_context
PATHS: [user.preferred_language, user.loyalty_tier]
```
### Session variables
Session variables hold data that is relevant only during a single conversation. They are created when the session starts and discarded when the session ends (unless `RESET: never` is specified).
#### Syntax
```yaml theme={null}
MEMORY:
session:
- variable_name
- variable_name
TYPE: string
DESCRIPTION: "Human-readable description"
INITIAL: "default_value"
RESET: per_session
```
You can declare a session variable with a bare name (minimal form) or with additional metadata.
#### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | `string` | Yes | -- | Variable name, used as the key in session context. |
| `TYPE` | `string`, `number`, `boolean`, `date`, `array`, `object` | No | -- | Value type for runtime validation and field resolution. |
| `DESCRIPTION` | `string` | No | -- | Human-readable description of the variable's purpose. |
| `INITIAL` | any | No | `null` | Initial value assigned when the session starts. (`initial_value` is accepted as a legacy alias.) |
| `RESET` | `per_session`, `per_activation`, `per_step`, `never` | No | `per_session` | When to reset the variable. See [Reset behavior](#reset-behavior). |
| `READONLY` | `boolean` | No | `false` | When `true`, the model's `__set_context__` cannot write the variable; `SET`, `on_result`, and hooks still can. See [Write authority](#write-authority). |
| `MODEL_WRITERS` | `string[]` | No | -- | Restricts which agents' models may write a shared variable. See [Model writers](#model-writers). |
| `MODEL_VISIBLE` | `boolean` | No | `true` | When `false`, excludes the variable from automatic model context. See [Model-visible session and persistent values](#model-visible-session-and-persistent-values). |
#### Reset behavior
| Value | Behavior |
| - | - |
| `per_session` | Variable is initialized at session start and cleared when the session ends. This is the default. |
| `per_activation` | Variable is reset each time the agent is (re)activated within a session (for example. on re-entry after a handoff). |
| `per_step` | Variable is reset to its initial value at the start of each flow step. Useful for step-scoped accumulators. |
| `never` | Variable persists for the lifetime of the runtime process. Use sparingly; prefer persistent memory instead. |
#### Example: typed session variables
```yaml theme={null}
MEMORY:
session:
- cart_items
TYPE: array
DESCRIPTION: "Items currently in the shopping cart"
INITIAL: []
RESET: per_session
- attempt_count
TYPE: number
INITIAL: 0
RESET: per_step
```
#### Write authority
**Removed:** `SOURCE`, `FORMAT`, and `DERIVED` on a session variable. A `SOURCE:`/`FORMAT:`/`DERIVED:` line (or the YAML `source:`/`format:`/`derived:` key) is now a compile error — `E493`, `E492`, and `E498` respectively. `DERIVED` was removed because its every-write recompute cost grew unbounded; `SOURCE`/`FORMAT` were removed alongside it. **Migration:** declare the field `READONLY: true` and compute/write it with an authored `SET`, `on_result` mapping, or hook `SET` (see below). Where you relied on `FORMAT` for kind inference, pass the kind as `MATCH_SCORE`/`MATCHES`/`MATCH_RESULT`'s explicit third argument instead — for example `MATCH_SCORE(said, stored, "email")`.
A session variable can declare `READONLY: true` so the model cannot write it, leaving an authored hook `SET` (or `on_result`, or a FLOW `SET`) to compute its value:
```yaml theme={null}
AGENT: Identity_Check
GOAL: "Confirm the caller before sharing account details"
MEMORY:
SESSION:
- NAME: supplied_email
TYPE: string
- NAME: profile_email
TYPE: string
- NAME: email_verified
TYPE: boolean
READONLY: true
HOOKS:
before_turn:
SET: email_verified = MATCHES(supplied_email, profile_email, "email")
```
| Property | Values | Meaning |
| - | - | - |
| `READONLY` | `true` | Opt-in. The model's `__set_context__` cannot write the variable; `SET`, `on_result` and hooks still can. Only an explicit `true` opts in. |
In YAML the same key is lowercase: `readonly: true`.
`MATCH_SCORE(said, stored, kind?)` returns 0-100 (or `-1` when there's nothing to judge — a missing value, a PII vault token, or an input over 1,024 characters; `-1` is below every threshold, so a rule written as `MATCH_SCORE(...) >= 70` fails closed). `MATCHES(said, stored, kind?)` is the pass/fail form on the platform's own match bands, and `MATCH_RESULT(said, stored, kind?)` returns `"match"`, `"retry"`, `"mismatch"`, or `"missing"`. These are a **said-versus-stored identity comparison**, not a regex test — the infix `a MATCHES "pattern"` operator is the regex form. `kind` is optional (default `text`); pass `"auto"` to detect the kind from the stored value's shape, or `"exact"` to turn off every voice allowance and require equality after normalization. See [Built-in function reference](/agent-platform/abl-reference/expressions-and-functions#built-in-function-reference) for the full signatures, `kind` values, and match bands.
#### Model writers
In a multi-agent project, several agents can share one session field — routing state, a verification result — and by default any agent's model can write it. `MODEL_WRITERS` names exactly which agents' models may:
```yaml theme={null}
AGENT: Coordinator
GOAL: "Collect and route the caller's request"
MEMORY:
SESSION:
- NAME: route_choice
TYPE: string
MODEL_WRITERS: [Coordinator]
- NAME: verified_result
MODEL_WRITERS: []
- ordinary_state
```
| Declaration | Model-write behavior |
| - | - |
| Omitted (project-wide) | No change — existing inferred protections (authentication, consent, gate ownership, tool-outcome fields) still apply; this is not a blanket exemption for everything else. |
| `MODEL_WRITERS: [Coordinator, Worker]` | Only the listed agents' models may write the field. |
| `MODEL_WRITERS: []` | Every model is denied; authored writes (`SET`, `on_result`, hooks) still work. |
The YAML key is `model_writers`. Names are case-sensitive agent names; every explicit declaration of the same shared field across the project must agree, and a conflicting pair fails closed (denied for everyone) rather than picking one. A full-project compile rejects a name that isn't a real agent in the project (`SESSION_MODEL_WRITER_UNKNOWN`); compiling a single agent in isolation defers that specific check. An invalid list (not an array of plain agent-name strings) is rejected at parse/compile time (`MEMORY_MODEL_WRITERS_INVALID`, `SESSION_MODEL_WRITERS_INVALID`) — the field is **not** left silently unprotected on invalid input.
A listed writer can clear an *inferred* return/gate ownership refusal, but it can never override an explicit `READONLY: true`, a private-identity refusal, already-captured consent, or a protected tool-call outcome.
**Authored proof, in brief.** Once a project declares any `MODEL_WRITERS`, a value arriving at a protected field through a `HANDOFF`/`DELEGATE` return `MAP`/`SET` must be traceable to something the platform itself produced — an executed tool result, or a value a hook/`on_result`/`ON_START`/`ESCALATE` action set directly. A value that is merely *model-written* (even copied through an ordinary `SET`), or sourced from raw caller input, secure-input collection, or a confirmation/inference step, does **not** count as proof — even if it's byte-identical to a previously proven value. A return assignment that can't establish this is skipped (the field keeps its prior value) and the runtime traces `RETURN_MODEL_WRITER_UNTRUSTED_SOURCE`. Prefer a static return `MAP`/`SET` path (a literal tool-result field) over a dynamic/computed expression when the source needs to flow into a writer-protected field — an expression the runtime can't statically resolve is treated conservatively and may be refused even when the underlying value is genuinely trustworthy.
An agent that declares any `MODEL_WRITERS` (including an empty `[]`) compiles to IR version `1.2`; an agent with none keeps its existing `1.0`/`1.1` version, and a project's compilation envelope carries the highest version among its agents. A Runtime that doesn't support `1.2` rejects that artifact outright — upgrade the compiler and the Runtime together before authoring or publishing `MODEL_WRITERS`.
#### Model-visible session and persistent values
A session or persistent entry can opt out of **automatic model context** with `MODEL_VISIBLE: false` (YAML: `model_visible: false`). Omitted or `true` preserves the existing automatic projection. The value stays fully available to deterministic expressions, conditions, tools, mappings, persistence, and recall — this is a placement control over what the model sees automatically, not a write-authority or access-scope change:
```yaml theme={null}
AGENT: Catalog_Assistant
GOAL: "Help the customer select an item"
MEMORY:
SESSION:
- NAME: catalog_mapping
TYPE: object
MODEL_VISIBLE: false
- NAME: selected_category
TYPE: string
PERSISTENT:
- PATH: user.internal_reference
SCOPE: user
ACCESS: read
MODEL_VISIBLE: false
```
An opted-out entry is left out of automatically serialized model context — including supported voice memory updates and LLM extraction grounding context. An explicit authored reference such as `{{catalog_mapping}}` can still place the value at the intended prompt location, subject to the usual access and sensitive-value rules — `MODEL_VISIBLE: false` is not a guarantee the value can never reach a model, only that it isn't included automatically. Deterministic rule evaluation and ML extraction always see the real value regardless of this setting. An excluded ancestor path excludes its descendants, even one marked `true`. The excluded path must be a literal, dot-separated path — numeric array positions are supported, but a wildcard, a bracket selector, an empty segment, or a `__proto__`/`prototype`/`constructor` segment is rejected (`MEMORY_MODEL_VISIBLE_PATH_UNSUPPORTED`) rather than silently left visible.
Each consuming agent's own memory declaration governs its automatic context. `MODEL_VISIBLE` requires matching authoring, parser/compiler, and Runtime versions deployed together — an older Runtime does not enforce it.
### Persistent variables
Persistent variables survive across sessions. They are stored in a fact store and scoped to either a specific user or to the entire project.
#### Syntax
```yaml expandable=true theme={null}
MEMORY:
persistent:
- user.preferred_chains
- user.wire_history_30d
SCOPE: user
TYPE: array
DESCRIPTION: "Recent wire transfer history"
ACCESS: readwrite
DEFAULT_VALUE: []
- user.ssn
SCOPE: user
SENSITIVE: true
SENSITIVE_DISPLAY: mask
MASK_CONFIG:
showFirst: 0
showLast: 4
char: "*"
- project.exchange_rates
SCOPE: project
ACCESS: read
```
Persistent variable paths use dot notation. The first segment typically indicates the scope (`user.*` or `project.*`), but you can override this with the `SCOPE` property.
#### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `path` | `string` | Yes | -- | Dot-notation path (for example. `user.preferred_chains`). |
| `SCOPE` | `user`, `project`, `execution_tree` | No | `user` | Ownership scope. See [Scoping rules](#scoping-rules). |
| `ACCESS` | `read`, `write`, `readwrite` | No | `readwrite` | Access direction. Constrains whether the agent can read, write, or both. |
| `TYPE` | `string`, `number`, `boolean`, `date`, `array`, `object` | No | -- | Value type for runtime validation. |
| `UNIT` | `string` | No | -- | Unit of the stored value (for example. `USD`, `km`). Informational metadata only. |
| `DEFAULT_VALUE` | any | No | `null` | Default value when no fact exists in the store. |
| `DESCRIPTION` | `string` | No | -- | Human-readable description. |
| `SENSITIVE` | `boolean` | No | `false` | Marks the value as PII for privacy-aware handling. |
| `SENSITIVE_DISPLAY` | `redact`, `mask`, `replace` | No | -- | How the value is displayed outside its context (when `SENSITIVE: true`). |
| `MASK_CONFIG` | `{showFirst, showLast, char}` | No | -- | Masking configuration when `SENSITIVE_DISPLAY` is `mask`. |
#### Scoping rules
* **User scope** (`SCOPE: user`): Values are unique per authenticated user. Two users in the same project see different values for the same path.
* **Project scope** (`SCOPE: project`): Values are shared across all users within the project. Useful for reference data, feature flags, or global configuration.
* **Execution-tree scope** (`SCOPE: execution_tree`): Values are shared across one handoff tree / long-running execution — visible to the agents participating in that orchestration but not persisted per-user or per-project.
As shorthand, a `READS:` sub-block declares persistent paths with `ACCESS: read`, and a `WRITES:` sub-block declares them with `ACCESS: write` — an alternative to listing each path under `persistent:` with an explicit `ACCESS`.
### Remember triggers
Remember triggers define rules for automatically storing values into persistent memory when a condition is met during conversation.
#### Syntax
```yaml theme={null}
MEMORY:
remember:
- WHEN: user_name IS SET
STORE: user_name -> user.name
TTL: "90d"
- WHEN: preferred_language IS SET
STORE: preferred_language -> user.language
```
Array targets can accumulate values instead of being overwritten, using the block form of `STORE`:
```yaml theme={null}
MEMORY:
remember:
- WHEN: search_completed
STORE:
value: last_query
target: user.recent_searches
operation: append # overwrite (default) | set | append
unique: true # optional — skip duplicates
maxSize: 10 # optional — FIFO cap
```
The modifiers are only read from the block form; the inline `value -> target`
arrow form always uses `overwrite`. `append`, `unique`, and `maxSize` apply to
`array`-typed targets in `user`, `project`, and `execution_tree` scopes.
#### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `WHEN` | `string` | Yes | -- | Condition expression that triggers the store operation. Evaluated after each turn. |
| `STORE` | `object` | Yes | -- | Value expression and target path. Inline syntax: `value_expr -> target_path`; block form to add modifiers. |
| `operation` | `overwrite`, `append` | No | `overwrite` | Array-write mode. `append` adds to the existing array; `overwrite` replaces the whole value. Block form only. |
| `unique` | `boolean` | No | `false` | When `append`, drop duplicate items (first occurrence kept; deep-equality for objects). Block form only. |
| `maxSize` | `number` | No | -- | When `append`, cap array length, evicting oldest items first (FIFO). Block form only. |
| `TTL` | `string` | No | -- | Time-to-live for the stored fact. After expiration, the value is deleted. Examples: `"30d"`, `"24h"`. |
#### TTL format
TTL values use a duration string:
| Suffix | Meaning |
| - | - |
| `s` | Seconds |
| `m` | Minutes |
| `h` | Hours |
| `d` | Days |
Example: `"90d"` means the stored fact expires 90 days after it was written.
### Recall instructions
Recall instructions define when and how to load persistent facts back into the session context. They execute at specific lifecycle events.
#### Syntax
```yaml theme={null}
MEMORY:
recall:
- ON: session:start
ACTION: inject_context
PATHS: [user.name, user.language, user.preferred_agent]
- ON: tool:search_flights:after
ACTION: load_memory
DOMAIN: "travel_preferences"
- ON: session:start
ACTION: prompt_llm
INSTRUCTION: "Greet the user by name if known"
```
#### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `ON` | `string` | Yes | -- | Lifecycle event that triggers the recall. See [Events](#recall-events). |
| `ACTION` | `string` | No | -- | The concrete action to execute. See [Actions](#recall-actions). |
| `INSTRUCTION` | `string` | Yes | -- | Human-readable instruction for the recall behavior. |
| `PATHS` | `array` | No | -- | Persistent paths to inject (for `inject_context` action). |
| `DOMAIN` | `string` | No | -- | Memory domain filter (for `load_memory` action). |
#### Recall events
`ON:` accepts these canonical lifecycle event patterns:
| Event pattern | When it fires |
| - | - |
| `session:start` | When a new session initializes, before any user input. |
| `session:end` | When a session ends. |
| `agent::before` | Before a specific agent runs (use `agent:*:before` for any). |
| `agent::after` | After a specific agent runs (use `agent:*:after` for any). |
| `tool::after` | After a specific tool runs (use `tool:*:after` for any). |
| `entity::extracted` | When a named entity/field is extracted from user input. |
| `step:enter:` | On entering a specific flow step. |
| `step:exit:` | On exiting a specific flow step. |
Legacy event names such as `search:before`, `ON_START`, or `ON_SEARCH` are no longer supported and are rejected during compilation — use the canonical patterns above. Note that `tool::before` is not a valid recall event (only `tool::after`).
#### Recall actions
| Action | Behavior |
| - | - |
| `inject_context` | Loads specified persistent paths directly into the session context as variables. |
| `load_memory` | Loads all facts in the specified domain into context. |
| `prompt_llm` | Passes the instruction text to the LLM as additional context. This is the default action. |
### Accessing memory in expressions
Session variables are accessed by name in expressions and template strings:
```yaml theme={null}
RESPOND: "Hello, {{customer_name}}! Your balance is {{available_balance}}."
```
Persistent variables are accessed through their full dot-notation path:
```yaml theme={null}
SET: greeting = COALESCE(user.name, "valued customer")
```
Variables set via `SET` assignments in flow steps, `ON_START`, or hooks are written to session memory and available for the remainder of the session.
***
## CONSTRAINTS
Constraints are deterministic business rules that the runtime evaluates against the current session state. Authors can still group constraints under named phases for readability, but phase names are currently labels only: the compiler flattens constraints into a single runtime list unless a constraint explicitly gates itself with `WHEN` or a structural checkpoint construct.
### Overview
Constraints enforce guardrails on agent behavior that are separate from LLM instructions. They are deterministic checks evaluated by the runtime, not suggestions to the model. A constraint that fails blocks the current operation and triggers an `ON_FAIL` action.
```yaml theme={null}
CONSTRAINTS:
always:
- REQUIRE customer_verified == true
ON_FAIL: "Please verify your identity first."
pre_booking:
- REQUIRE amount <= available_balance
ON_FAIL: "Insufficient funds. Your available balance is {{available_balance}}."
- REQUIRE sanctions_clear == true
ON_FAIL: HANDOFF Compliance_Officer
```
### Phases
Named phases are retained as authoring labels for readability, review, and organization. They do not currently create separate runtime execution phases by themselves.
#### Syntax
```yaml theme={null}
CONSTRAINTS:
phase_name:
- REQUIRE condition_expression
ON_FAIL: action_or_message
```
Use labels like `always`, `pre_booking`, or `eligibility_check` when they help readers understand intent, but use `WHEN` to gate applicability and `BEFORE` only for explicit structural checkpoint targets.
### Requirement rules
Each requirement within a phase uses one of three keywords: `REQUIRE`, `LIMIT`, or `RESTRICT`.
#### REQUIRE
The most common form. Asserts that a condition must be true. If the condition evaluates to false, the `ON_FAIL` action is triggered.
```yaml theme={null}
- REQUIRE account_status == "active"
ON_FAIL: "Wire transfers require an active account."
```
#### LIMIT
Expresses a numeric boundary. Semantically equivalent to `REQUIRE` with a comparison, but communicates intent more clearly.
```yaml theme={null}
- LIMIT daily_wire_used + amount <= daily_wire_limit
ON_FAIL: "This would exceed your daily wire limit of {{daily_wire_limit}}."
```
#### RESTRICT
Expresses a prohibition. The condition describes what is forbidden; when it evaluates to true, the constraint fails.
```yaml theme={null}
- RESTRICT beneficiary_country IN ["CU", "IR", "KP", "SY"]
ON_FAIL: "Transfers to that destination are prohibited under sanctions regulations."
```
#### WHEN
Use `WHEN:` to make a constraint apply only in a specific context without overloading the phase label.
```yaml theme={null}
- REQUIRE ssn IS NOT SET
WHEN: channel == "voice"
ON_FAIL: "SSN cannot be collected on voice."
```
#### BEFORE
Use `BEFORE` for structural checkpoints that the runtime knows how to activate.
```yaml theme={null}
- REQUIRE measure_field IS SET BEFORE calling search_aggregate
ON_FAIL: "Select a measure before running the aggregate search."
- REQUIRE aggregation_validated == true BEFORE returning results
ON_FAIL: "Validate the aggregation before responding."
```
Supported structural targets today:
* `BEFORE calling `
* `BEFORE returning results`
Non-structural `BEFORE` targets are retained for compatibility, but they are warning-only and have no runtime effect. Prefer `IMPLIES` or `WHEN` for that form.
A `BEFORE calling ` rule's `condition` (`REQUIRE`/`LIMIT`/`RESTRICT`) or `WHEN` can read the about-to-be-sent arguments via `tool_args`, but only when the agent sets `EXECUTION: guardrail_builtins: true`. Without it, `tool_args` is unbound: the rule cannot read the arguments, so it never holds, and the tool call is refused. The compiler warns `GUARDRAIL_BUILTIN_NOT_BOUND` for this case.
```yaml theme={null}
EXECUTION:
guardrail_builtins: true
CONSTRAINTS:
always:
- REQUIRE tool_args.amount <= daily_wire_limit BEFORE calling wire_transfer
ON_FAIL: "That exceeds your daily wire limit."
```
#### IMPLIES
`IMPLIES` is an inline condition operator (not a top-level keyword) for expressing conditional requirements. `A IMPLIES B` is lowered to `NOT (A) OR (B)` — that is, "if A holds, then B must also hold." Use it inside the condition of a `REQUIRE`/`LIMIT`/`RESTRICT` rule:
```yaml theme={null}
- REQUIRE transfer_type == "international" IMPLIES swift_code IS SET
ON_FAIL: "International transfers require a SWIFT/BIC code."
```
#### ON\_FAIL actions
`ON_FAIL` accepts a single-line action — a message string, `HANDOFF `, or `ESCALATE` — or a structured block. The block form can respond, reset state, and route in one place:
```yaml theme={null}
- REQUIRE amount <= daily_wire_limit
ON_FAIL:
RESPOND: "That exceeds your daily limit. Let's re-enter the amount."
CLEAR: [amount, raw_amount]
GOTO: collect_amount
```
| Key | Type | Description |
| - | - | - |
| `RESPOND` | `string` | Message sent to the user on failure. |
| `CLEAR` | `string[]` | Session variables to reset on failure, so stale values are recollected. |
| `COLLECT` | `string[]` | Fields to re-collect before retrying. |
| `GOTO` | `string` | Step to route to. A single identifier naming a declared FLOW step; anything else is spoken literally. |
| `RETRY` | `boolean` | Whether to retry the failed operation. |
| `MAX_REPEATS` | `number` | Failures of this constraint (per conversation) this block answers before `ON_EXHAUSTED` takes over. Omit for no cap. |
| `ON_EXHAUSTED` | string or action | Inline action run once `MAX_REPEATS` is used up, for example `ESCALATE "identity_not_confirmed"`. |
The `CLEAR` key lets an `ON_FAIL` block reset dependent state directly — you no longer need a separate `CLEAR`-only step solely to clear variables before a retry.
An `ON_FAIL` `RESPOND` message may use `{{_abl_constraint_fail_count}}` — this failure's 1-based ordinal for the constraint within the current conversation. Pair it with `MAX_REPEATS`/`ON_EXHAUSTED` to vary the message as attempts run out, then hand off once the cap is reached:
```yaml theme={null}
- REQUIRE MATCH_SCORE(supplied_email, profile_email) >= 70
ON_FAIL:
RESPOND: "That doesn't match what's on file (attempt {{_abl_constraint_fail_count}})."
MAX_REPEATS: 2
ON_EXHAUSTED: ESCALATE "identity_not_confirmed"
```
### Constraint properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `condition` | `string` | Yes | -- | Expression to evaluate. See [Expressions & functions](/agent-platform/abl-reference/expressions-and-functions). |
| `WHEN` | `string` | No | -- | Optional applicability gate. The compiler lowers it to implication semantics so the constraint only applies when the gate is true. |
| `ON_FAIL` | `string` or action | Yes | `respond` | Action when the condition fails. See [ON\_FAIL actions](#on_fail-actions). |
| `severity` | `error` or `warning` | No | `error` | `error` blocks execution; `warning` emits a warning but continues. In text ABL, use the `WARN` keyword instead of a `severity:` line. |
### ON\_FAIL actions
The `ON_FAIL` value determines what happens when a constraint fails.
#### String message (respond)
The most common form. Sends a message to the user and halts the current operation.
```yaml theme={null}
ON_FAIL: "Insufficient funds. Available balance: {{available_balance}}."
```
#### HANDOFF
Transfers control to another agent. Active flow and reasoning runtime checkpoints execute the handoff immediately when the constraint fails.
```yaml theme={null}
ON_FAIL: HANDOFF Compliance_Officer
```
#### ESCALATE
Triggers human escalation.
```yaml theme={null}
ON_FAIL: ESCALATE
```
#### Block
Silently blocks the operation without a user-facing message.
```yaml theme={null}
ON_FAIL: BLOCK
```
#### Structured ON\_FAIL block
For more control, use a structured block that combines multiple actions.
```yaml theme={null}
- REQUIRE payment_method IS SET
ON_FAIL:
RESPOND: "I need your payment method before proceeding."
COLLECT: [payment_method]
THEN: retry
```
##### Structured ON\_FAIL properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `RESPOND` | `string` | No | -- | Message to send to the user. |
| `COLLECT` | `string[]` | No | -- | Fields to collect from the user before re-evaluating. |
| `GOTO` | `string` | No | -- | Flow step to jump to. |
| `RETRY` | `boolean` | No | `false` | Retry the current step after handling the constraint. |
| `THEN` | `continue` or `retry` | No | -- | What to do after collection: `continue` proceeds, `retry` re-checks. |
| `MAX_REPEATS` | `number` | No | -- | Failures of this constraint (per conversation) answered before `ON_EXHAUSTED` takes over. |
| `ON_EXHAUSTED` | string or action | No | -- | Inline action run once `MAX_REPEATS` is used up. |
#### Collect and retry example
```yaml theme={null}
CONSTRAINTS:
pre_booking:
- REQUIRE departure_date IS SET AND return_date IS SET
ON_FAIL:
RESPOND: "I need your travel dates to check availability."
COLLECT: [departure_date, return_date]
THEN: retry
- REQUIRE departure_date > NOW()
ON_FAIL:
RESPOND: "The departure date must be in the future. What date works for you?"
COLLECT: [departure_date]
THEN: retry
```
#### GOTO example
```yaml theme={null}
- REQUIRE user.verified == true
ON_FAIL:
RESPOND: "You need to verify your identity first."
GOTO: identity_verification_step
```
### Severity levels
| Severity | Behavior |
| - | - |
| `error` | Blocks the current operation. The `ON_FAIL` action executes. This is the default. |
| `warning` | Emits a warning event and continues execution. The `ON_FAIL` message is logged, not sent. |
```yaml theme={null}
- WARN fraud_score < 60
ON_FAIL: "Transaction flagged for review."
```
### Constraint evaluation order
1. Constraint phase names such as `always` or `pre_booking` are labels only; the compiler flattens all constraints into one runtime list.
2. `WHEN` gates a constraint to a specific context, and structural `BEFORE` gates it to supported checkpoints such as tool calls or final responses.
3. Inline flow-step `CHECK` expressions are separate from `CONSTRAINTS` and evaluate directly against `step.check`.
4. Declaration order is preserved in the flattened list.
5. Evaluation stops at the first `error`-severity failure. Warnings do not halt evaluation.
### Template interpolation in messages
`ON_FAIL` message strings support `{{variable}}` interpolation using the current session context:
```yaml theme={null}
- REQUIRE (daily_wire_used + amount) <= daily_wire_limit
ON_FAIL: "This wire would exceed your daily limit of {{daily_wire_limit}}. You've sent {{daily_wire_used}} today."
```
***
**Related articles:**
* [Guardrails](/agent-platform/abl-reference/guardrails) -- input/output safety checks (distinct from business rule constraints)
* [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) -- condition expression syntax
* [Multi-Agent and Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) -- HANDOFF and ESCALATE actions referenced in ON\_FAIL
* [Lifecycle and hooks](/agent-platform/abl-reference/lifecycle-and-hooks) -- `ON_START` handler where initial memory setup occurs
* [Data types](/agent-platform/abl-reference/data-types-and-utilities) -- type definitions used in `TYPE` declarations
# Multi-agent and supervisor
Source: https://koreai.mintlify.app/agent-platform/abl/reference/multi-agent-and-supervisor
This page documents multi-agent orchestration constructs (`DELEGATE`, `HANDOFF`, `ESCALATE`, `COMPLETE`) and the `SUPERVISOR:` declaration for top-level routing.
***
## SUPERVISOR declaration
A Supervisor is a top-level orchestrator that routes user messages to the appropriate child agent based on intent, context, and declarative rules. Supervisor documents use the `SUPERVISOR:` keyword instead of `AGENT:` and define agent references, routing rules, state schemas, policies, and communication settings.
### Overview
While agents handle domain-specific tasks, the Supervisor decides which agent should handle each user message. It does not execute tools or gather information directly; it classifies intent and routes accordingly.
```yaml theme={null}
SUPERVISOR: Travel_Supervisor
VERSION: "2.0"
DESCRIPTION: "Routes customers to booking, support, or sales specialists"
GOAL: "Route requests to the right specialist with full context preservation"
PERSONA: |
Professional travel booking assistant. Friendly, efficient, and helpful.
Routes requests quickly and transparently.
```
### Agent references
The Supervisor declares which agents are available for routing. Each reference includes a file path, an alias, and a list of capabilities.
#### Syntax
```yaml expandable=true theme={null}
AGENTS:
- REF: ./agents/flight_search.agent.abl
ALIAS: Flight_Search
CAPABILITIES: [flight_booking, fare_search, seat_selection]
CHANNELS: [web, mobile, voice]
REQUIRES_VALIDATION: false
- REF: ./agents/support.agent.abl
ALIAS: Support_Agent
CAPABILITIES: [booking_management, cancellation, refund]
REQUIRES_VALIDATION: true
```
#### Agent reference properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `REF` | `string` | Yes | -- | File path to the agent's ABL definition. |
| `ALIAS` | `string` | Yes | -- | Local name used in routing rules and handoff targets. |
| `CAPABILITIES` | `string[]` | Yes | -- | List of capability tags describing what the agent can do. |
| `CHANNELS` | `string[]` | No | -- | Channels this agent supports (for example. `web`, `mobile`, `voice`). |
| `REQUIRES_VALIDATION` | `boolean` | No | `false` | Whether the user must be authenticated before routing to this agent. |
### Routing rules
Routing rules define conditional logic for directing messages to agents. Rules are evaluated in priority order.
#### Syntax
```yaml theme={null}
ROUTING:
- NAME: escalation_route
DESCRIPTION: "Route frustrated or explicitly requesting human"
PRIORITY: 1
WHEN: intent.category == "escalation" OR user.frustration_detected == true
THEN: ROUTE_TO Live_Agent_Transfer
FLAGS: [set_active]
- NAME: booking_route
PRIORITY: 5
WHEN: intent.category == "new_booking"
THEN: ROUTE_TO Sales_Agent
```
Alternatively, routing can be declared using the `HANDOFF:` block within a Supervisor, following the same syntax as [agent handoffs](#handoff):
```yaml theme={null}
HANDOFF:
- TO: Sales_Agent
WHEN: intent.category == "new_booking"
CONTEXT:
pass: [search_context, user_preferences, budget]
summary: "User looking to book new travel"
EXPECT_RETURN: false
```
#### Routing rule properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `NAME` | `string` | Yes | -- | Unique name for the routing rule. |
| `DESCRIPTION` | `string` | No | -- | Human-readable description. |
| `PRIORITY` | `number` | Yes | -- | Evaluation order. Lower values are evaluated first. |
| `WHEN` | `string` | Yes | -- | Condition expression that must be true for this rule to activate. |
| `THEN` | `RoutingAction` | Yes | -- | Action to take. See [Routing actions](#routing-actions). |
| `FLAGS` | `string[]` | No | -- | Behavioral flags. See [Routing flags](#routing-flags). |
| `CONSTRAINTS` | `object` | No | -- | Additional constraints on when this rule applies. |
#### Routing actions
| Action | Syntax | Description |
| - | - | - |
| Route to agent | `ROUTE_TO Agent_Name` | Send the message to a specific agent. |
| Route to user | `ROUTE_TO_USER "message"` | Send a message and wait for user input. |
| Route by variable | `ROUTE_TO_VARIABLE var_name` | Route to the agent named in a variable. |
| Intent-based routing | `INTENT_MATCH` | Route based on detected intent. See below. |
| End conversation | `END_CONVERSATION` | End the session. |
| System action | `SYSTEM_ACTION action_name` | Execute a system-level action (for example. handoff). |
#### Intent-based routing
For more granular routing, use `INTENT_MATCH` with intent-to-agent mappings:
```yaml expandable=true theme={null}
ROUTING:
- NAME: intent_router
PRIORITY: 10
WHEN: true
THEN:
INTENT_MATCH:
- INTENTS: [flight_search, hotel_search]
ACTION: ROUTE_TO Sales_Agent
- INTENTS: [manage_booking, cancel_booking]
ACTION: ROUTE_TO Support_Agent
FALLBACK: ROUTE_TO Fallback_Handler
```
#### Routing flags
| Flag | Effect |
| - | - |
| `set_active` | Mark the target agent as the active agent for subsequent messages. |
| `silent` | Route without sending a user-visible message. |
| `no_log` | Do not log this routing decision in the trace store. |
| `priority_boost` | Apply a priority boost in the target agent's queue. |
#### Conditional routing (WHEN clauses)
WHEN clauses use the same expression syntax as [Expressions & functions](/agent-platform/abl-reference/expressions-and-functions). Common patterns include:
```yaml expandable=true theme={null}
# Intent-based
WHEN: intent.category == "complaint"
# State-based
WHEN: user.is_authenticated == true AND intent.category == "manage_booking"
# Negation
WHEN: NOT intent.has_specific_request
# Compound
WHEN: intent.unclear == true OR intent.confidence < 0.5
# Variable check
WHEN: handoff_count >= 4
```
#### Routing constraint blocks
Add constraints to limit when a rule applies beyond the WHEN condition:
```yaml expandable=true theme={null}
ROUTING:
- NAME: voice_only_route
PRIORITY: 3
WHEN: intent.category == "voice_action"
THEN: ROUTE_TO Voice_Agent
CONSTRAINTS:
channels: [voice]
requiresState: [user.is_authenticated]
ignoreIntents: [greeting, farewell]
```
| Constraint | Type | Description |
| - | - | - |
| `ignoreIntents` | `string[]` | Intents that should not trigger this rule. |
| `channels` | `string[]` | Channels where this rule applies (for example. `[web, voice]`). |
| `requiresState` | `string[]` | State variables that must be set for this rule to activate. |
### State schema
The Supervisor can declare a state schema that defines typed variables organized by namespace.
```yaml expandable=true theme={null}
STATE:
user:
is_authenticated:
type: boolean
required: false
default: false
description: "Whether the user has been authenticated"
language:
type: string
required: false
source: user
system:
routing_failures:
type: number
required: false
default: 0
source: system
```
State variables follow the same [VariableDefinition](/agent-platform/abl-reference/data-types-and-utilities) structure as agent variables, with the addition of namespace grouping.
#### State variable properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | See [Data types](/agent-platform/abl-reference/data-types-and-utilities) | Yes | -- | Variable type. |
| `required` | `boolean` | No | `false` | Whether the variable must have a value. |
| `default` | any | No | -- | Default value. |
| `description` | `string` | No | -- | Human-readable description. |
| `source` | `system`, `user` , `agent`, `computed` | No | -- | Origin of the variable value. |
| `updatedBy` | `string[]` | No | -- | List of agent aliases that can update this variable. |
### Policies
Policies define high-level behavioral rules for the Supervisor, constraining what it is allowed and forbidden to do.
```yaml expandable=true theme={null}
POLICIES:
- NAME: no_direct_booking
DESCRIPTION: "Supervisor must not make bookings directly"
RULES:
forbiddenWhen: intent.category == "booking" AND active_agent IS NOT SET
behavior: "Route to Sales_Agent instead of handling directly"
- NAME: auth_required_for_account
DESCRIPTION: "Account operations require authentication"
RULES:
allowedWhen: user.is_authenticated == true
triggerSignal: "auth_required"
```
#### Policy properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `NAME` | `string` | Yes | -- | Unique policy name. |
| `DESCRIPTION` | `string` | No | -- | Human-readable description. |
| `RULES` | `object` | Yes | -- | Policy rule definitions. See below. |
#### Policy rule properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `allowedWhen` | `string` | No | -- | Condition under which the action is allowed. |
| `forbiddenWhen` | `string` | No | -- | Condition under which the action is forbidden. |
| `triggerSignal` | `string` | No | -- | Signal to emit when the policy is triggered. |
| `behavior` | `string` | No | -- | Description of the expected behavior when triggered. |
### Communication settings
Communication settings define the Supervisor's language, tone, and vocabulary preferences.
```yaml expandable=true theme={null}
COMMUNICATION:
language: en
formality: neutral
pronouns:
use: "we"
avoid: "I"
vocabulary:
prefer: [assist, help, guide]
avoid: [unfortunately, regrettably]
constraints:
- "Never reveal internal agent names to the user"
- "Always explain why a transfer is happening"
```
#### Communication properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `language` | `string` | Yes | -- | Primary language code (for example. `en`, `es`). |
| `formality` | `formal`, `informal`, `neutral` | Yes | -- | Communication tone. |
| `pronouns` | `object` | No | -- | Pronoun preferences (`use` and `avoid`). |
| `vocabulary` | `object` | No | -- | Preferred and avoided vocabulary. |
| `constraints` | `string[]` | Yes | -- | Communication behavioral constraints. |
### Behavior settings
The `BEHAVIOR` block declares the Supervisor's execution **role**, which governs how it participates
in coordination.
```yaml theme={null}
BEHAVIOR:
role: orchestrator # router | orchestrator | assistant
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `role` | `string` | No\* | -- | Execution role: `router`, `orchestrator`, or `assistant`. See [Execution roles](#execution-roles). |
| `canRespondDirectly` | `boolean` | No | -- | **Deprecated** — migrate to `role`. Retained as a compatibility fallback (see below). |
| `allowedDirectActions` | `string[]` | No | `[]` | **Deprecated** compatibility field; does not authorize runtime operations. |
| `forbiddenActions` | `string[]` | No | `[]` | **Deprecated** compatibility field; does not authorize runtime operations. |
\*`role` is optional, but omitting it on an agent that has a `BEHAVIOR` block produces a deprecation warning and falls back to legacy inference. Declaring `role` is the recommended, forward-compatible form.
#### Execution roles
| Role | Responds directly? | Can `DELEGATE`? | Meaning |
| - | - | - | - |
| `router` | No | No | Routes each message to a child agent via `HANDOFF`. Requires at least one `HANDOFF` target. |
| `orchestrator` | Yes | Yes | Coordinates child agents and may also synthesize/respond directly. Required to use `DELEGATE`. |
| `assistant` | Yes (leaf) | Yes | A leaf agent that handles conversations directly. **Not valid on a `SUPERVISOR` document.** |
**Breaking changes in this release - Artemis 1.4.0:**
* `canRespondDirectly` is deprecated in favor of `role`. If both are present and **conflict**
(`canRespondDirectly: true` ↔ `orchestrator`, `false` ↔ `router`), compilation fails
(`AGENT_EXECUTION_ROLE_CONFLICT`). Remove the deprecated field or pick the matching role.
* A `SUPERVISOR` must be `router` or `orchestrator` — `role: assistant` fails compilation.
* `role: router` requires at least one `HANDOFF` ownership target.
* Only an `orchestrator` may declare `DELEGATE:` — a `router` (or `assistant`) that uses
`DELEGATE` fails compilation (`AGENT_RETURNED_WORK_REQUIRES_ORCHESTRATOR`). Routers hand off;
orchestrators own returned work.
### Intents
In addition to `INTENT_MATCH` inside a routing rule, a Supervisor may declare a top-level `INTENTS:` block listing intent labels (and optional lexical-fallback behavior) that routing and classification draw on. Supervisors can also hand off to **other supervisors**, enabling hierarchical composition, and may declare an `ON_ERROR` block to handle routing failures.
### Execution pipeline (pre-classification)
This is an advanced, opt-in optimization. Most supervisors do not need it — routing works without an `EXECUTION` block.
A Supervisor can enable a pre-classification pipeline that runs a smaller, faster model before the main reasoning LLM. The classifier detects user intent and can short-circuit routing for obvious cases, avoiding the cost of a full reasoning call.
```yaml expandable=true theme={null}
SUPERVISOR: Support_Router
EXECUTION:
model: claude-sonnet-4-5-20250929 # main reasoning model
pipeline:
enabled: true
mode: sequential # 'parallel' | 'sequential'
shortCircuit:
enabled: true
confidenceThreshold: 0.85
toolFilter:
enabled: true
maxTools: 6
keywordVeto:
enabled: true
keywords: [reset, cancel, undo]
intentBridge:
enabled: true
programmaticThreshold: 0.85
guidedThreshold: 0.5
outOfScopeDecline: true
multiIntentSignal: true
HANDOFF:
- TO: Billing_Agent
WHEN: intent.category == "billing"
- TO: General_Inquiry
WHEN: true
```
The classifier **model** is not selected in the agent's `EXECUTION.pipeline` block — it is resolved from **project-level runtime configuration** via `modelSource` (`default` uses the platform's tool-selection model; `tenant` uses a specific tenant model identified by `tenantModelId`). A `model:` key inside the `pipeline` block is deprecated and ignored by the runtime. The top-level `EXECUTION.model` still sets the **main reasoning** model as usual.
#### Pipeline options
| Option | Type | Default | Description |
| - | - | - | - |
| `enabled` | Boolean | `false` | Enable the pre-classification pipeline. |
| `mode` | String | `parallel` | `parallel` — classifier and main LLM run simultaneously; `sequential` — classifier runs first, main LLM only if no short-circuit. |
| `shortCircuit.enabled` | Boolean | `true` | Allow direct routing when classifier confidence is high. |
| `shortCircuit.confidenceThreshold` | Number | `0.85` | Minimum confidence to skip the reasoning loop. |
| `toolFilter.enabled` | Boolean | `true` | Filter tools to only relevant ones before reasoning. |
| `toolFilter.maxTools` | Number | `6` | Maximum tools passed to the reasoning loop. |
| `keywordVeto.enabled` | Boolean | `true` | Prevent short-circuit when the user mentions local tool keywords. |
| `keywordVeto.keywords` | String | `[]` | Additional keywords that veto short-circuit routing. |
| `intentBridge.enabled` | Boolean | `true` | Bridge classifier intent into routing decisions. |
| `intentBridge.programmaticThreshold` | Number | `0.85` | Confidence at/above which intent routing is applied programmatically. |
| `intentBridge.guidedThreshold` | Number | `0.5` | Confidence at/above which intent is offered to the model as guidance. |
| `intentBridge.outOfScopeDecline` | Boolean | `true` | Decline out-of-scope requests detected by the classifier. |
| `intentBridge.multiIntentSignal` | Boolean | `true` | Signal when the message contains multiple distinct intents. |
For pure routing supervisors, `sequential` mode with `shortCircuit.enabled: true` gives the best cost savings. In `parallel` mode the classifier adds latency protection but no cost savings, since both calls run regardless.
***
## HANDOFF
HANDOFF transfers conversational control from the current agent to another agent, passing context and optionally expecting a return.
When a handoff enters a target agent whose entry point is a REASONING zone (or a scripted flow step), the target begins executing **immediately on entry** — it does not wait for a new user message. This lets a receiving agent act on the forwarded context (see `history`, below) as soon as control transfers.
### Syntax
```yaml expandable=true theme={null}
HANDOFF:
- TO: Compliance_Officer
WHEN: sanctions_clear == false
PRIORITY: 0
CONTEXT:
pass: [customer_id, beneficiary_name, amount, sanctions_match_score]
summary: "Wire flagged during sanctions screening (score: {{sanctions_match_score}})."
history: full
memory_grants:
- path: user.compliance_notes
access: readwrite
EXPECT_RETURN: true
ON_FAILURE: ESCALATE
ON_RETURN:
action: continue
map:
compliance_decision: sanctions_clear
review_notes: compliance_review_notes
```
### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `TO` | `string` | Yes | -- | Target agent name. |
| `WHEN` | `string` | Yes | -- | Condition that triggers the handoff. |
| `ENABLED_WHEN` | `string` | No | -- | Condition deciding whether this route exists for the session at all, evaluated once before any turn. See [Availability (ENABLED\_WHEN)](#availability-enabled_when). |
| `PRIORITY` | `number` | No | -- | Evaluation priority. Lower values are evaluated first when multiple handoff rules match. |
| `CONTEXT` | `object` | Yes | -- | Context to pass to the target agent. See [Context](#handoff-context). |
| `EXPECT_RETURN` | `boolean` | Yes | `false` | Whether control should return to this agent after the target completes. `RETURN` is accepted as a backward-compatible alias. |
| `ON_FAILURE` | `string` / `object` | No | -- | Parent-side fallback for setup or dispatch failures **before** the target accepts the handoff. See [Handoff failure strategies](#handoff-failure-strategies). |
| `ON_RETURN` | `string` / `object` | No | -- | How to handle the target returning control. See [Return expectations](#return-expectations). |
| `EXPERIENCE_MODE` | `string` | No | -- | End-user experience when the transfer happens. See [Experience modes](#experience-modes). |
| `REMOTE` | `object` | No | -- | Remote agent configuration. Can also be expressed as top-level `LOCATION`/`ENDPOINT`/`PROTOCOL` keys. See [Remote agent support](#remote-agent-support). |
| `ASYNC` | `boolean` | No | `false` | Use async dispatch with push notifications for remote agents. |
| `ASYNC_TIMEOUT` | `number` | No | -- | Timeout in seconds for async handoff. (Also accepted as `TIMEOUT`.) |
The primary keyword is **`EXPECT_RETURN`**. The older `RETURN` keyword is still parsed for backward compatibility, but new agents should use `EXPECT_RETURN`. The legacy top-level `MAP` block is superseded by `ON_RETURN.map` (see [Return expectations](#return-expectations)).
### Handoff context
The `CONTEXT` block defines what information the target agent receives.
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `pass` | `string[]` | Yes | -- | Session variable names to include in the handoff context. |
| `summary` | `string` | Yes | -- | Human-readable summary of why the handoff is occurring. Supports `{{}}` interpolation. |
| `history` | `string` / `object` | No | `full` | Conversation history strategy. See [History strategies](#history-strategies). |
| `set` | `Record` | No | -- | Route-scoped variable assignments applied **before** the target agent is invoked (inject computed values without a separate step). |
| `memory_grants` | `object[]` | No | -- | Persistent memory paths to grant the target agent access to. See [Memory grants](#memory-grants). |
The legacy `grant_memory: [path, ...]` form is **no longer supported** and produces a parse error. Use `memory_grants` with explicit `path`/`access` entries instead (see [Memory grants](#memory-grants)).
`CONTEXT` may also be expressed as a shorthand where `pass`, `summary`, and `history` appear as direct siblings of the handoff entry rather than nested under `CONTEXT:`.
### History strategies
The `history` property controls how much conversation history the target agent receives.
| Value | Behavior |
| - | - |
| `full` | **Platform default** — applied when `history` is omitted. The complete parent conversation history is passed. |
| `auto` | Uses the handoff summary when available; otherwise falls back to a bounded recent transcript (the last 5 messages). |
| `none` | No conversation history is passed. The target agent starts fresh. |
| `summary_only` | Only the `summary` text is passed, no raw messages. |
| `{ mode: last_n, count: }` | The last N messages are passed. |
```yaml theme={null}
CONTEXT:
pass: [customer_id, amount]
summary: "Customer needs fraud review."
history:
mode: last_n
count: 10
```
The legacy shorthand `history: last_10` is still accepted during the compatibility window, but new agents should use the typed `mode` + `count` block.
### Return expectations
When `EXPECT_RETURN: true`, the calling agent pauses and waits for the target agent to complete.
When the target explicitly returns control (via its built-in return capability), the parent resumes.
When `EXPECT_RETURN: false`, the handoff is a one-way transfer and the calling agent's turn ends.
`ON_RETURN` controls what happens when the target returns. It accepts either a **string** (the name of a named return handler, or a built-in action) or a **structured block**:
```yaml expandable=true theme={null}
HANDOFF:
- TO: Authentication_Agent
WHEN: user.is_authenticated == false
CONTEXT:
pass: [session_context]
summary: "User needs authentication"
EXPECT_RETURN: true
ON_RETURN:
action: continue # continue | resume_intent
handler: route_to_booking # optional: named RETURN_HANDLERS entry
resume_with: latest_session_user_utterance
map: # child result field -> parent variable
user_id: auth_result.user_id
auth_token: auth_result.token
set: # parent assignments after mapping
auth_summary: "Authenticated {{auth_result.user_id}}"
```
#### `ON_RETURN` properties
| Property | Type | Description |
| - | - | - |
| `action` | `string` | Built-in resume behavior: `continue` (resume the parent's current flow) or `resume_intent` (re-run routing against the original user intent with the returned context). |
| `handler` | `string` | Name of a [named return handler](#return-handlers) declared in the top-level `RETURN_HANDLERS:` block. |
| `resume_with` | `string` | Which utterance to replay when resuming. One of `original_handoff_utterance`, `latest_child_user_utterance`, `latest_session_user_utterance`, `forwarded_return_message`. |
| `map` | `Record` | Copies child return fields into parent variables (child key → parent variable). |
| `set` | `Record` | Parent assignments applied after `map`, using the same templated assignment shape as flow `ON_RESULT` branches. |
### Return handlers
A top-level `RETURN_HANDLERS:` block declares reusable named handlers that run on the parent after a `EXPECT_RETURN: true` child returns. Reference one from `ON_RETURN.handler`.
```yaml theme={null}
RETURN_HANDLERS:
route_to_booking:
RESPOND: "You're verified — let's finish your booking."
CLEAR: [pending_auth_reason]
CONTINUE: true # leave the parent waiting for the next user turn
# RESUME_INTENT: true # or replay the parent's last user message
```
| Property | Type | Description |
| - | - | - |
| `RESPOND` | `string` | Message to send when the handler runs. |
| `CLEAR` | `string[]` | Session variables to reset after the child returns. |
| `CONTINUE` | `boolean` | Leave the parent waiting for the next user turn. |
| `RESUME_INTENT` | `boolean` | Replay the parent's last user message through routing. |
### Handoff failure strategies
`ON_FAILURE` defines a parent-side fallback for failures that occur **before** the target accepts the handoff — for example target lookup, pre-transfer validation, or dispatch failures. It does not replace the timeout path after an accepted returnable handoff, and it does not fire when a downstream child later reports its own failure.
```yaml theme={null}
HANDOFF:
- TO: Specialist_Agent
WHEN: needs_specialist == true
CONTEXT:
pass: [user_id, query]
summary: "User needs specialist help"
ON_FAILURE: ESCALATE # CONTINUE | ESCALATE | RESPOND "message"
```
| Value | Behavior |
| - | - |
| `CONTINUE` | Silently continue in the current agent. |
| `ESCALATE` | Trigger human escalation. |
| `RESPOND "message"` | Send a message and continue in the current agent. |
### Experience modes
`EXPERIENCE_MODE` shapes how the transfer is presented to the end user. It is valid on both `HANDOFF` and `DELEGATE`.
| Value | Behavior |
| - | - |
| `shared_voice_handoff` | Voice channels — hand off while keeping a single shared voice experience. |
| `visible_handoff` | The user is told the conversation is being transferred. |
| `silent_delegate` | The sub-agent runs without a user-visible transfer. |
| `human_escalation` | Present the transfer as an escalation to a human. |
### Async dispatch
For remote agents, set `ASYNC: true` to dispatch the handoff asynchronously. The calling agent receives a notification when the remote agent completes rather than blocking.
```yaml expandable=true theme={null}
HANDOFF:
- TO: External_Review
WHEN: requires_external_review == true
CONTEXT:
pass: [case_id, documents]
summary: "Documents require external review."
EXPECT_RETURN: true
REMOTE:
location: remote
endpoint: "https://review.partner.com/api/v1/submit"
protocol: a2a
ASYNC: true
ASYNC_TIMEOUT: 3600
```
### Memory grants
Use `memory_grants` to give the target agent scoped access to specific persistent memory paths. Without this, the target agent cannot read or write the parent's persistent variables. Each grant declares a `path` and an `access` level.
```yaml expandable=true theme={null}
CONTEXT:
pass: [user_id, booking_reference]
summary: "User needs to authenticate to manage booking."
memory_grants:
- path: user.last_verified_at
access: read
- path: workflow.auth_token
access: readwrite
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `path` | `string` | Yes | -- | Persistent memory path being granted. |
| `access` | `string` | No | `read` | Access level: `read` or `readwrite`. |
***
## DELEGATE
DELEGATE invokes a sub-agent synchronously, waits for it to complete, and maps the result back into the calling agent's context. The sub-agent runs in its own scope and does not have direct access to the parent's session variables.
**Breaking changes in this release - Artemis 1.4.0:**
* `DELEGATE` may only be declared by an [`orchestrator`](#execution-roles). A `router` or
`assistant` that uses `DELEGATE` fails compilation.
* `DELEGATE` is a one-shot internal call — it cannot wait for multi-turn user input. If the target
agent has **required** `GATHER` fields, you must supply them in `INPUT:` (or make them
optional/defaulted). An unmapped required field fails compilation
(`DELEGATE_CHILD_GATHER_INPUT_UNMAPPED`); model a user-interactive child as an
`EXPECT_RETURN: true` `HANDOFF` instead.
### Syntax
```yaml expandable=true theme={null}
DELEGATE:
- AGENT: Sanctions_Screening
WHEN: beneficiary_name IS SET AND beneficiary_country IS SET
PURPOSE: "Screen beneficiary against OFAC SDN and EU sanctions lists"
INPUT:
name: beneficiary_name
account: beneficiary_account
country: beneficiary_country
RETURNS:
cleared: sanctions_clear
match_score: sanctions_match_score
USE_RESULT: "Block if match_score > 85. Proceed only if cleared."
TIMEOUT: "15s"
ON_FAIL: escalate
```
### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `AGENT` | `string` | Yes | -- | Name of the sub-agent to invoke. `TO` is accepted as an alias. |
| `WHEN` | `string` | Yes | -- | Condition that must be true for delegation to occur. |
| `ENABLED_WHEN` | `string` | No | -- | Condition deciding whether this route exists for the session at all, evaluated once before any turn. See [Availability (ENABLED\_WHEN)](#availability-enabled_when). |
| `PURPOSE` | `string` | Yes | -- | Description of why this delegation exists. Included in trace events. `SUMMARY` is accepted as an alias. |
| `INPUT` | `Record` | Yes | -- | Input mapping. Keys are sub-agent parameter names; values are expressions from the parent's context. `PASS` is accepted as an alias. |
| `RETURNS` | `Record` | Yes | -- | Output mapping. Keys are sub-agent result fields; values are parent context variable names to write to. |
| `USE_RESULT` | `string` | Yes | -- | Instructions for interpreting the sub-agent's result. |
| `MEMORY_GRANTS` | `object[]` | No | -- | Persistent memory paths to grant the sub-agent, each with `path` and `access` (`read` / `readwrite`). Same shape as [handoff memory grants](#memory-grants). |
| `TIMEOUT` | `string` | No | -- | Maximum time to wait for the sub-agent. Format: `"Ns"` (seconds). |
| `ON_FAIL` | `string`, `object` | No | -- | Action when the sub-agent fails or times out. See [Failure strategies](#delegate-failure-strategies). |
| `EXPERIENCE_MODE` | `string` | No | -- | End-user experience for the delegation. See [Experience modes](#experience-modes). |
| `REMOTE` | `object` | No | -- | Remote agent configuration. Can also be given as top-level `LOCATION`/`ENDPOINT`/`PROTOCOL` keys. See [Remote agent support](#remote-agent-support). |
| `FAILURE_MESSAGE` | `string` | No | -- | Message to display when delegation fails. |
### Input/output mapping
The `INPUT` block maps values from the parent agent's session context into the sub-agent's input parameters:
```yaml theme={null}
INPUT:
name: beneficiary_name # parent's beneficiary_name -> sub-agent's "name" parameter
account: beneficiary_account
amount: amount
```
The `RETURNS` block maps the sub-agent's result fields back into the parent's session variables:
```yaml theme={null}
RETURNS:
cleared: sanctions_clear # sub-agent's "cleared" field -> parent's sanctions_clear
match_score: sanctions_match_score
```
### Delegate failure strategies
| Value | Behavior |
| - | - |
| `respond` | Send a message and continue. Requires `FAILURE_MESSAGE`. |
| `continue` | Silently continue without the sub-agent's result. |
| `escalate` | Trigger human escalation. |
| `retry` | Retry the delegation. Accepts a `count` for max retries. |
Structured failure example:
```yaml theme={null}
ON_FAIL:
type: retry
count: 2
```
### Remote agent support
DELEGATE supports invoking agents running on remote services. Add a `REMOTE` block to configure the connection.
```yaml expandable=true theme={null}
DELEGATE:
- AGENT: External_Compliance
WHEN: amount > 10000
PURPOSE: "External compliance check for high-value transfers"
INPUT:
transaction_id: wire_reference
amount: amount
RETURNS:
approved: compliance_approved
USE_RESULT: "Proceed only if approved."
REMOTE:
location: remote
endpoint: "https://compliance.partner.com/api/v1/check"
protocol: a2a
auth:
type: bearer
timeout: "30s"
```
#### Remote properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `location` | `local`, `remote` | No | `local` | Whether the agent is co-located or remote. |
| `endpoint` | `string` | No | -- | URL for remote agent invocation. |
| `protocol` | `a2a` , `rest` | No | -- | Communication protocol. `a2a` for Agent-to-Agent, `rest` for REST API. |
| `auth` | `object` | No | -- | Authentication configuration. `type` is `bearer` or `api_key`; an optional `header` names the header to carry the credential. Credential values are resolved from runtime config / auth-profile resolution — never inlined in ABL. |
| `timeout` | `string` | No | -- | Override timeout for the remote call. |
`LOCATION: remote` is explicit and recommended, but the compiler also treats an `ENDPOINT` without `LOCATION` as remote. The remote block can be authored either nested under `REMOTE:` or as top-level `LOCATION`/`ENDPOINT`/`PROTOCOL` keys on the entry.
### Availability (`ENABLED_WHEN`)
`ENABLED_WHEN` turns a `HANDOFF` or `DELEGATE` route on or off for the whole session, decided once before any turn is understood. `WHEN` still decides which available route a given turn takes — `ENABLED_WHEN` decides which routes exist at all:
```yaml theme={null}
INTENTS:
vehicles: "The customer is looking for automotive options."
products: "The customer is looking for product options."
HANDOFF:
- TO: Automotive_Search
WHEN: intent.category == "vehicles"
ENABLED_WHEN: _metadata.country != "SGP"
ON_FAILURE: RESPOND "Vehicle search is not offered in your region."
- TO: Product_Search
WHEN: intent.category == "products"
```
**Writing the condition.** Put `ENABLED_WHEN:` on the entry, after `WHEN:` (YAML key: `enabled_when`). Read only session data known before the turn: `_metadata.*`, `env.*`, `caller.*`, `session.*`, `user.*`, and memory fields. Don't read what the current turn means (`intent`, `routing.*`, `input`) or write natural language — both are compile errors (`ENABLED_WHEN_SEMANTIC_REFERENCE`). A long condition can continue on deeper-indented lines, after an inline value, an empty value, or a block indicator (`|`, `>`, `>-`); a line that looks like a key or starts a new entry ends it, and `#` lines are comments. The compiler warns (`ROUTE_WHEN_MULTILINE_JOINED`) when it joins lines written after an inline or empty value; a condition opened with a block indicator is joined without the warning.
**How availability is decided.** Each target is judged by its own entries: a `HANDOFF` to X by the `HANDOFF` entries for X, a `DELEGATE` to X by the `DELEGATE` entries for X. The target is available when *any* of its entries is enabled — an entry without `ENABLED_WHEN` is always enabled, so one unconditional entry keeps the target permanently available (the compiler warns, `ENABLED_WHEN_SHADOWED_BY_UNCONDITIONAL_ENTRY`).
The decision **fails closed**: a condition that can't be evaluated disables the entry, and so does one that reads a value the session doesn't have (unset, `null`, or an empty string) — even inside `OR` — *unless* the condition explicitly allows that value to be missing. A value compared with `== null`, `!= true`, or `IS NOT SET` anywhere in the condition counts as known, and an unset value then reads as `null`:
```yaml theme={null}
ENABLED_WHEN: _metadata.restricted != true # enabled when the caller sends nothing
ENABLED_WHEN: _metadata.country != "SGP" # disabled when country is missing
ENABLED_WHEN: _metadata.region IS NOT SET OR _metadata.region != "EU" # guard explicitly
```
`_metadata` changes made during the session apply to every agent in the conversation, including agents waiting for a child to return.
**When a target isn't available.** The route is withheld from the model entirely — left out of prompt route lists in both text and voice. A model that calls it anyway is refused and told which routes are available. A route the *author* wrote (a FLOW step `HANDOFF`, `ON_ACTION`, a digression, a tool hook, `ON_ERROR`, an `ON_START` handoff) is refused and runs its own failure policy: the entry's `ON_FAILURE` for a handoff, the delegate's `ON_FAILURE` for a delegate. `RESPOND` shows its message, `ESCALATE` escalates the same turn, `CONTINUE` skips the route and runs the rest of the step. A handoff with no `ON_FAILURE` tells the user the option isn't available and the agent stays on its current step; a delegate with no `ON_FAILURE` is simply skipped — a refused route never leaves a turn without an answer. A constraint's `ON_FAIL`/`ON_EXHAUSTED` handoff is a safety route that must always be reachable, so its target can't be fully conditional: the compiler rejects one whose every `HANDOFF` entry has `ENABLED_WHEN` (`ENABLED_WHEN_SAFETY_ROUTE_CONDITIONAL`). When every route of an agent is unavailable, the agent answers the user directly.
**Diagnostics**
| Code | Severity | Meaning |
| - | - | - |
| `ENABLED_WHEN_EMPTY` | error | `ENABLED_WHEN` has no condition. |
| `ENABLED_WHEN_LINE_COMMENT` | error | A YAML `enabled_when` read as one line carries a `//` comment (use `#`, or a literal block). |
| `ROUTE_WHEN_MULTILINE_JOINED` | warning | A condition without a block indicator was read from several lines. |
| `ENABLED_WHEN_SEMANTIC_REFERENCE` | error | The condition reads turn meaning, or is natural language. |
| `INVALID_CEL_SYNTAX` | error | The condition isn't a valid expression. |
| `ENABLED_WHEN_ALL_DISABLED_RISK` | warning | Every route of a routing agent is conditional. |
| `ENABLED_WHEN_SHADOWED_BY_UNCONDITIONAL_ENTRY` | warning | A target has entries both with and without `ENABLED_WHEN`. |
| `ENABLED_WHEN_AUTHORED_ROUTE_NO_FALLBACK` | warning | An authored route targets a conditional handoff with no `ON_FAILURE`. |
| `ENABLED_WHEN_SAFETY_ROUTE_CONDITIONAL` | error | A constraint `ON_FAIL`/`ON_EXHAUSTED` handoff targets a fully conditional handoff. |
| `ENABLED_WHEN_ON_START_UNSTABLE_DATA` | warning | An `ON_START` delegate's condition reads data that may not exist yet at session start. |
`_metadata` is supplied by the caller. Use `ENABLED_WHEN` to shape the experience, not as an authorization boundary. Availability is always enforced — there's no runtime setting that turns it off, and an entry without `ENABLED_WHEN` behaves exactly as before this feature existed. The trace records which routes were offered and withheld on each model turn, and every refusal, with the condition text — never the session values it evaluated.
***
## Parallel delegation (fan-out)
There is no `FAN_OUT:` section in ABL. Fan-out is a runtime capability, not an authored construct — agents do not declare it directly.
When a single user message contains **multiple distinct requests** that need different specialists, a supervisor's runtime can dispatch them in parallel and then synthesize the branch results into one unified response. This is exposed to the reasoning model as a built-in `__fan_out__` system tool (you never write this tool yourself) and is bounded to **2–5 sub-tasks** per message.
Separately, when multiple `DELEGATE` entries have their `WHEN` conditions satisfied on the same turn, the runtime can run those delegations concurrently. Each delegation runs in its own scope and its `RETURNS` are mapped back into the parent context as it completes.
```yaml expandable=true theme={null}
DELEGATE:
- AGENT: Sanctions_Screening
WHEN: beneficiary_name IS SET
PURPOSE: "Screen beneficiary"
INPUT:
name: beneficiary_name
RETURNS:
cleared: sanctions_clear
- AGENT: Fraud_Detection
WHEN: amount IS SET
PURPOSE: "Score fraud risk"
INPUT:
amount: amount
account: source_account
RETURNS:
score: fraud_score
```
***
## ESCALATE
ESCALATE transfers the conversation to a human operator. It is designed for situations where the agent cannot or should not continue autonomously.
### Syntax
```yaml expandable=true theme={null}
ESCALATE:
triggers:
- WHEN: sanctions_screening_unavailable == true AND retry_count >= 2
REASON: "Sanctions screening service down. Compliance check cannot be bypassed."
PRIORITY: critical
TAGS: [compliance, service_outage]
- WHEN: user.wants_human_agent == true
REASON: "Customer requesting human specialist."
PRIORITY: medium
TAGS: [human_request]
context_for_human:
- customer_id
- customer_name
- amount
- conversation_history
routing:
connection: livechat
queue: "wire_operations_l2"
skills: [wire_transfer, compliance]
priority: 1
voice: # voice-channel transfer settings (see below)
transfer_method: refer
call_transfer_type: sip
sip_headers:
X-Escalation-Reason: "sanctions_review"
on_human_complete:
- IF human.resolved == true: COMPLETE
- IF human.needs_agent == true: HANDOFF to specified_agent
CONNECTOR_ACTION: create_servicenow_incident
```
### Trigger properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `WHEN` | `string` | Yes | -- | Condition that triggers escalation. |
| `REASON` | `string` | Yes | -- | Human-readable reason for the escalation. |
| `PRIORITY` | `string` / `number` | No | `medium` | Priority level: `low`, `medium`, `high`, `critical`, or a non-negative integer. |
| `TAGS` | `string[]` | No | -- | Tags for routing and categorization in the human agent queue. |
| `SET` | `Record` | No | -- | Session variable assignments applied when this trigger fires (`key = value`). |
| `GENESYS_SIP_HEADERS` | `object` | No | -- | Per-trigger override of the Genesys SIP transfer headers attached when this escalation transfers a voice call. See [Genesys SIP transfer headers](#genesys-sip-transfer-headers). |
`PRIORITY` is optional and defaults to `medium`. Note that `ESCALATE` inside an `ON_ERROR` handler is **not** parsed — to escalate from an error handler use `THEN: ESCALATE with REASON: "..."` instead of an inline `ESCALATE:` block.
### Connector action
`CONNECTOR_ACTION` names a connector action to invoke for ITSM integration when the escalation fires (for example, opening an incident in an external ticketing system). It is declared at the top level of the `ESCALATE` block, alongside `triggers`, `context_for_human`, `routing`, and `on_human_complete`.
### Priority levels
| Level | Use case |
| - | - |
| `low` | Non-urgent requests (for example. general feedback). |
| `medium` | Standard requests (for example. customer asks to speak with a human). |
| `high` | Urgent issues (for example. service outages, repeated failures). |
| `critical` | Immediate attention required (for example. compliance violations, fraud). |
### Context for human
The `context_for_human` block lists session variable names to include in the escalation package. The human agent sees these values in their interface.
```yaml theme={null}
context_for_human:
- customer_id
- customer_name
- source_account
- amount
- fraud_score
- conversation_history
```
You can also use structured context items with templates:
```yaml theme={null}
context_for_human:
- NAME: case_summary
TEMPLATE: "Customer {{customer_name}} requesting wire of {{amount}} {{currency}}"
INCLUDE: [fraud_score, sanctions_match_score]
```
### Routing configuration
The `routing` block controls how the escalation is routed in the human agent system.
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `connection` | `string` | No | -- | Human-agent connection/channel to route through. |
| `queue` | `string` | No | -- | Target queue name in the human agent system. |
| `skills` | `string[]` | No | -- | Required skills for the human agent. |
| `priority` | `number` | No | -- | Numeric routing priority for the queue entry. |
| `post_agent` | `string` | No | -- | What happens after the human agent finishes: `return` or `end`. |
| `voice` | `object` | No | -- | Voice-channel transfer settings — how the live call leg is handed off at the SIP/transport layer. See [Voice transfer settings](#voice-transfer-settings). |
Earlier drafts used `skill_tags` and `priority_boost`. The current runtime contract uses `skills` and `priority` — the older keys are silently ignored. The `routing` block also accepts several [advanced routing keys](#advanced-routing-keys) for richer human-agent integrations.
#### Conditional routing entries
`routing:` accepts two shapes, distinguished purely by what follows the colon:
* A **mapping** (`routing: { connection: ..., queue: ..., ... }`, as above) — a single, unconditional destination. This is the legacy, still-supported form.
* A **YAML sequence** (a list of `-`-prefixed entries) — an **ordered list of conditional routing entries**, each with the same fields as the single form plus `id` and `when`. When authored as a list, `routing` itself is left unset and the entries compile into `routing_entries` instead.
```yaml theme={null}
ESCALATE:
triggers:
- WHEN: intent.category == "human_escalation"
REASON: "Caller requested a live agent"
PRIORITY: high
routing:
- id: whatsapp
WHEN: system.channel == "whatsapp"
connection: "Genesys Agent Transfer"
- id: voice_es
WHEN: system.channel == "voice" && system.locale == "es"
voice:
call_transfer_type: sip
transfer_method: refer
sip_transfer_id: "sip:es_billing_queue@sbc.example.com"
- id: fallback
WHEN: always
connection: "Default Queue"
```
| Field | Type | Description |
| - | - | - |
| `id` | `string` | Optional label for authoring/observability. Defaults to `entry_` if omitted. |
| `when` | `string` | Condition selecting this entry, in the same deterministic expression grammar as trigger `WHEN` clauses (comparisons, `AND`/`OR`/`NOT`, `IS [NOT] SET`, `has(...)`, `abl.(...)`) — **not** free text. `WHEN` (uppercase) is also accepted. Omit, or use `always` / `*`, for the unconditional default entry. |
Every other field (`connection`, `queue`, `skills`, `priority`, `post_agent`, `voice`, and all the [advanced routing keys](#advanced-routing-keys)) means exactly the same thing per entry as it does in the single-object form.
A routing entry's `when` must be **deterministic** — it's evaluated in code against session data and the reserved `system.*` values (`system.channel`, `system.locale`, and similar), never deferred to the model. This is stricter than a trigger's `WHEN`, which may be prompt-only/semantic: a condition that's valid on a `triggers.WHEN` but isn't deterministic is a **compile-time error** on a routing entry (`ESCALATE_ROUTING_WHEN_PROMPT_ONLY`) — a live transfer destination can never depend on model judgment.
**Evaluation is first-match-wins, in authored order.** The compiler enforces the entry list's shape:
| Rule | Enforcement |
| - | - |
| At most one unconditional (default) entry | More than one entry with no `when` (or `always`/`*`) is a compile error. |
| A default entry, if present, must be last | A default entry followed by other entries is a compile error — those entries would be unreachable. |
| No entry is required to match | A list with no default entry is valid; this isn't a compile-time exhaustiveness check. If none of the entries' conditions match at runtime, routing **fails closed**: no connector or voice transfer is attempted (the escalation's `HumanTask`/HITL fallback still stands). List-form routing never falls back to the project's `agentTransfer.defaultRouting` connection — that backstop applies only to the legacy single-object `routing:` form. To reach the project default from a list, author an explicit trailing default entry (omit `when`, or use `when: always`) with your own `connection`/`voice`. |
An entry whose `WHEN` guards `system.channel == "voice"` without a `voice:` block, or guards a non-voice channel without a `connection`, gets an advisory (non-blocking) warning — worth checking, since a matched entry with neither is unlikely to route anywhere useful.
##### `sip_transfer_id`
A voice routing entry (or the legacy single `routing.voice` block) may declare an explicit SIP transfer destination directly in the DSL with `sip_transfer_id` — a literal or `{{ }}`-templated SIP URI:
```yaml theme={null}
voice:
call_transfer_type: sip
sip_transfer_id: "sip:{{escalation_skill}}@sbc.example.com"
```
This requires `call_transfer_type: sip` — declaring `sip_transfer_id` under `pstn` or `custom` is a compile error (PSTN transfers always use the project's PSTN transfer target, not a SIP transfer id). The compiler **always** stamps the resulting destination's provenance as `config` itself — an authored `sip_transfer_id_source` value is discarded and never trusted verbatim, the same trust posture as the `call_transfer` tool's `default_source: config` gate. Omit `sip_transfer_id` to keep today's behavior: the transfer target comes from the project-configured Agent Transfer settings.
#### Advanced routing keys
Beyond `connection` / `queue` / `skills` / `priority`, the `routing` block accepts additional keys for richer human-agent (contact center) integrations. Some are structured; others (`flow_policy`, `provider_config`, `metadata`) are **opaque pass-throughs** whose shape is defined by the receiving human-agent integration rather than validated by the ABL compiler. Every key accepts `snake_case` or `camelCase`.
```yaml expandable=true theme={null}
ESCALATE:
routing:
connection: livechat
queue: "wire_operations_l2"
skills: [wire_transfer, compliance]
priority: 1
# Route to specific named human agents, optionally waiting for one to be free
named_agents: ["jane.doe", "john.smith"]
named_agent_options:
wait_for_agent: true
wait_duration_seconds: 120
# Constrain which agents may pick up the conversation
agent_matching_conditions:
skills: [wire_transfer]
skill_groups: [compliance_l2]
agent_groups: [emea_desk]
# Provider-defined routing sub-type and adapter configuration
sub_type: "priority_callback"
provider_config:
campaign_id: "Q3-collections"
disposition_required: true
# Wait / no-agent behavior, declared in a TRANSFER_FLOW_POLICIES block
flow_policy_ref: standard_wait
# Opaque key-value pairs forwarded on the transfer payload's metadata
metadata:
case_id: "{{case_id}}"
segment: "gold"
triggers:
- WHEN: user.wants_human_agent == true
REASON: "User requested human support"
PRIORITY: high
```
| Key | Type | Description |
| - | - | - |
| `sub_type` | `string` | Provider-defined routing sub-type / channel hint for the human-agent system. |
| `named_agents` | `string[]` | Request specific human agents by name or id. |
| `named_agent_options` | `object` | Options that apply when `named_agents` is set: `wait_for_agent` (`boolean`) and `wait_duration_seconds` (`number`). |
| `agent_matching_conditions` | `object` | Constrain who may receive the transfer: `skills`, `skill_groups`, and `agent_groups` (each `string[]`). |
| `flow_policy_ref` / `transfer_flow_policy_ref` | `string` | Name of a policy declared in a `TRANSFER_FLOW_POLICIES:` block that governs wait / no-agent behavior. See [Transfer flow policies](#transfer-flow-policies). |
| `flow_policy` / `transfer_flow_policy` | `object` | The same policy authored **inline** instead of by reference. Free-form; shape defined by the integration. |
| `provider_config` | `object` | Opaque provider-specific adapter configuration. Shape defined by the human-agent integration, not the ABL parser. |
| `metadata` | `object` | Opaque key-value pairs delivered with the escalation on the transfer payload's metadata. Supports `{{ }}` interpolation. Reserved keys (`reason`, `priority`, flow policy) are always set by the runtime and cannot be overridden here. |
These keys are hints for the receiving human-agent system — the valid `sub_type` values, `provider_config` shape, and inline `flow_policy` structure are defined by that integration, not the ABL compiler. Voice-channel transfer settings live under `routing.voice` (see [Voice transfer settings](#voice-transfer-settings)).
#### Voice transfer settings
When an escalation routes over a **voice** channel, the optional `routing.voice` block controls how the live call leg is handed off at the SIP/transport layer. It is ignored on non-voice channels.
```yaml theme={null}
ESCALATE:
routing:
connection: livechat
queue: "wire_operations_l2"
voice:
transfer_method: refer # invite | refer | bye
call_transfer_type: sip # sip | pstn | custom (omit sip/pstn to infer from project config)
sip_headers: # forwarded to the receiving SBC / contact center
X-Escalation-Reason: "sanctions_review"
X-Priority: "1"
```
| Field | Type | Description |
| - | - | - |
| `transfer_method` | `"invite"` \| `"refer"` \| `"bye"` | SIP signaling method used to transfer the active call leg. |
| `call_transfer_type` | `"sip"` \| `"pstn"` \| `"custom"` | Whether to move the call over SIP or PSTN, or run a **custom** tool sequence (see below). Omit `sip`/`pstn` to infer from project configuration — SIP when a `sipTransferTargetUri` is configured, PSTN when only a `pstnTransferTargetNumber` is. |
| `sip_headers` | `Record` | Custom SIP headers forwarded on the transfer to the receiving SBC or contact center. |
| `sip_transfer_id` | `string` | Explicit SIP transfer destination URI (literal or `{{ }}`-templated). Requires `call_transfer_type: sip`. See [Conditional routing entries](#sip_transfer_id) above. |
| `custom_tools` | `string[]` | Required when `call_transfer_type: custom` — an ordered list of existing project tool names run as the transfer sequence. See [Custom voice transfer](#custom-voice-transfer). |
Without `sip_transfer_id`, `call_transfer_type` selects a **transport, not a destination** — the transfer target is project-owned (Agent Transfer settings), and authored DSL cannot name a phone number or SIP URI, the same boundary the built-in `call_transfer` tool enforces by default. `sip_transfer_id` is the one explicit exception to that rule (see above). For Genesys Audio Connector transfers, use the dedicated `genesys_sip_headers` block below rather than `sip_headers`.
#### Custom voice transfer
`call_transfer_type: custom` (ABLP-3552, CXone/NICE integration) replaces the SIP/PSTN leg transfer with an ordered sequence of **project tools**. Instead of moving the call, the runtime keeps the Artemis audio leg live and runs the named tools serially as a notification sequence — for example, obtaining a token from one HTTP tool and signaling an external contact-center API with the next.
```yaml theme={null}
ESCALATE:
triggers:
- WHEN: caller.requests_human == true
REASON: "Caller requested an agent"
PRIORITY: high
routing:
voice:
call_transfer_type: custom
custom_tools:
- nice_token # first tool may capture a SecretRef result
- signal_api # next tool may consume that reference
```
* `custom_tools` is an ordered list of **1–8** exact, existing project **HTTP** tool names, invoked in the authored order. The first tool may capture a [SecretRef](/agent-platform/abl/reference/tools#reuse-secrets-across-tools-secretref) that a later tool consumes.
* `custom` routing is **mutually exclusive** with a `connection` — a custom escalation has no SIP/PSTN connection.
* The compiler validates shape (blocking errors): the transfer type must be `sip`/`pstn`/`custom`; `custom` requires 1–8 `custom_tools`; each name must be a non-empty ASCII identifier (`^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$`); names must be unique. Tool **existence and HTTP kind** are enforced at runtime (a non-HTTP or unknown tool is rejected then).
Custom voice transfer is **notification-only** in this version — it signals an external system while the caller stays on the Artemis leg; it does not hand the live call off to a human queue. Use `sip`/`pstn` (or `ESCALATE` routing to a human connection) when the call itself must be transferred.
#### Genesys SIP transfer headers
When an escalation transfers a **voice** call to Genesys (through the Genesys Audio Connector), `genesys_sip_headers` attaches custom SIP headers to the transfer — carrying the target queue, a reason code, and optional metadata. Declare a default under `routing:` and/or an override on individual triggers with the `GENESYS_SIP_HEADERS:` block. A trigger's block takes precedence over the routing default.
```yaml expandable=true theme={null}
ESCALATE:
triggers:
- WHEN: intent.category == "billing_dispute"
REASON: "Billing dispute requires a specialist."
PRIORITY: high
GENESYS_SIP_HEADERS: # per-trigger override
queue_header_key: "X-Genesys-Queue"
queue_name: "billing_disputes"
reason_header_key: "X-Genesys-Reason"
reason: "billing_dispute"
metadata_header_key: "X-Genesys-Data"
metadata:
tier: "gold"
region: "us-east"
routing:
connection: livechat
queue: "billing_l2"
genesys_sip_headers: # routing-level default for all triggers
queue_header_key: "X-Genesys-Queue"
queue_name: "billing_default"
```
Each field is optional. The `*_header_key` fields name a SIP header; the paired field supplies its value.
| Field | Type | Description |
| - | - | - |
| `queue_header_key` | `string` | SIP header **name** that carries the queue. |
| `queue_name` | `string` | Value placed in the queue header. |
| `reason_header_key` | `string` | SIP header **name** that carries the reason. |
| `reason` | `string` | Value placed in the reason header. |
| `metadata_header_key` | `string` | SIP header **name** that carries the metadata. |
| `metadata` | `string` or `object` | A string is sent verbatim; a map is serialized to a compact JSON string. |
The compiler validates the block against the same limits the runtime transfer enforces:
* Header-name fields (`*_header_key`) must be RFC 3261 tokens, at most 64 characters.
* Header values (`queue_name`, `reason`, and the serialized `metadata`) must contain no CR/LF/NUL characters and stay within 1024 UTF-8 bytes.
* `metadata` must be a string or a non-empty map — arrays and scalar values are rejected; an empty `{}` is ignored.
* Two header-name fields must not resolve to the same SIP header key (compared case-insensitively). A collision (`GENESYS_SIP_HEADER_KEY_COLLISION`) is a compile error, because the transfer rejects colliding header keys at runtime.
* Unrecognized fields are ignored with a warning. Keys may be written in `snake_case` or `camelCase` (for example `queue_header_key` or `queueHeaderKey`).
#### Transfer flow policies
`routing.flow_policy_ref` (or `transfer_flow_policy_ref`) names a policy declared in a top-level `TRANSFER_FLOW_POLICIES:` block. A transfer flow policy defines what happens while a caller waits in queue or when no agent is available — for example, playing a message or triggering a follow-up workflow. The policy body is free-form (its shape is defined by the human-agent integration, not the ABL parser).
```yaml expandable=true theme={null}
TRANSFER_FLOW_POLICIES:
standard_wait:
on_queued:
type: message
text: "Hello {{user.first_name}}, you're in the queue — a specialist will join shortly."
on_no_agents:
type: workflow
workflow_id: no_agents_followup
ESCALATE:
routing:
connection: livechat
queue: support
flow_policy_ref: standard_wait
triggers:
- WHEN: user.wants_human_agent == true
REASON: "User requested human support"
PRIORITY: high
```
### Post-completion actions
The `on_human_complete` block defines what happens after the human agent finishes.
```yaml theme={null}
on_human_complete:
- IF human.resolved == true: COMPLETE
- IF human.needs_agent == true: HANDOFF to specified_agent
- IF human.needs_followup == true: CONTINUE
```
Each entry has a `condition` and an `action`. The action can be `COMPLETE` (end the conversation), `HANDOFF` (transfer to another agent), or `CONTINUE` (resume the current agent).
When the human agent closes the conversation, a `FEEDBACK` survey card can be shown — authored either on the agent itself or inside a `FLOW:` step. See [Post-transfer surveys](/agent-platform/abl-reference/rich-content#post-transfer-surveys-escalate-to-a-human-agent) for exactly which block is picked and when.
***
## COMPLETE
COMPLETE defines the conditions under which the agent considers its task finished. Each completion condition specifies a `WHEN` expression and an optional response.
### Syntax
```yaml expandable=true theme={null}
COMPLETE:
- WHEN: confirmation_number IS SET AND transfer_status == "released"
RESPOND: |
Your wire transfer has been executed successfully.
**Confirmation:** {{confirmation_number}}
**Amount:** {{amount}} {{currency}}
**To:** {{beneficiary_name}}
**Estimated Arrival:** {{estimated_arrival}}
STORE: "wire_transfers"
- WHEN: transfer_status == "queued"
RESPOND: "Your wire has been queued for the next processing window."
```
### Properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `WHEN` | `string` | Yes | -- | Boolean condition over session state. The agent completes when this evaluates to true. |
| `RESPOND` | `string` | No | -- | Final message to the user. Supports `{{}}` template interpolation. |
| `SET` | `Record` | No | -- | Session assignments applied when the completion fires (for example, clear the sticky flag that triggered it). |
| `TERMINAL` | `boolean` | No | `true` | When `false`, the response renders (including interactive widgets like a closing feedback collector) **without** ending the session — control returns to the agent for the next turn. |
| `STORE` | `string` | No | -- | Collection or path name to store the completion result for analytics. |
| `MODEL_CONTEXT` | `string` | No | -- | Text retained for the model instead of the delivered `RESPOND` in future model calls. See [MODEL\_CONTEXT on a response](/agent-platform/abl-reference/flow#model_context-on-a-response). |
`COMPLETE`'s `WHEN` is **deterministic** — it must be a boolean expression over session state. Plain-English / intent-style text is rejected at compile time (`PLAIN_ENGLISH_COMPLETE_WHEN`). Use `NLU`/routing for intent matching, not `COMPLETE WHEN`. A fully-quoted `WHEN` (`WHEN: "status == \"done\""`) is unwrapped and evaluated the same as an unquoted one when it contains an operator — see [Quoting a condition](/agent-platform/abl-reference/expressions-and-functions#quoting-a-condition).
A `COMPLETE` condition that matches does **not** complete the agent if the agent's own `CONSTRAINTS` fail on the state it would complete with — the failing constraint's `ON_FAIL` runs instead, so a completion can never bypass a constraint that would otherwise block the same state.
Example of a non-terminal completion that collects feedback and then keeps the session open:
```yaml theme={null}
COMPLETE:
- WHEN: feedback_requested == true
RESPOND: "Thanks! How would you rate this conversation?"
SET:
feedback_requested: "false"
TERMINAL: false
```
Always clear the condition that triggered a `COMPLETE` block when using `TERMINAL: false` (for example, by using `SET`). Otherwise, the same completion condition may be evaluated again on the next user message, creating a completion loop. When such a loop is detected, the platform automatically closes the session and logs a warning.
### Rich content in completion
COMPLETE responses support voice configuration and rich content, the same as any RESPOND:
```yaml expandable=true theme={null}
COMPLETE:
- WHEN: booking_confirmed == true
RESPOND: "Your booking is confirmed!"
VOICE:
ssml: "Your booking is confirmed! "
RICH_CONTENT:
MARKDOWN: |
## Booking Confirmed
| Detail | Value |
|--------|-------|
| Reference | {{booking_ref}} |
| Date | {{travel_date}} |
ACTIONS:
- id: view_itinerary
type: button
label: "View Itinerary"
```
`FORMATS`/`RICH_CONTENT`, `VOICE`, and `ACTIONS` are recognized regardless of where they appear in the condition — immediately after `RESPOND`, after `STORE`/`SET`/`TERMINAL`, or on a condition with **no** `RESPOND` at all (for example, a completion that renders only a `FEEDBACK` card):
```yaml theme={null}
COMPLETE:
- WHEN: transfer_status == "released"
STORE: "wire_transfers"
RICH_CONTENT:
FEEDBACK:
FEEDBACK_TEMPLATE: Post_Chat_NPS
```
### Completion evaluation
Completion conditions are evaluated after every turn, in declaration order. The first matching condition triggers completion. If no condition matches, the agent continues the conversation.
***
## Context passing
### Evaluation order across constructs
When multiple multi-agent constructs apply on the same turn, the runtime evaluates them in this order:
1. **ESCALATE triggers** -- checked first; critical safety and compliance.
2. **HANDOFF rules** -- evaluated by priority (lower first).
3. **DELEGATE conditions** -- evaluated in declaration order.
4. **COMPLETE conditions** -- checked last.
***
## Complete supervisor example
```yaml expandable=true theme={null}
SUPERVISOR: Customer_Service_Hub
VERSION: "2.0"
GOAL: "Route customers to the right specialist"
PERSONA: |
Professional customer service coordinator. Routes requests
efficiently and preserves context across agent transfers.
HANDOFF:
- TO: Live_Agent
WHEN: intent.category == "escalation"
CONTEXT:
pass: [user_id, conversation_summary]
summary: "User requests human assistance"
EXPECT_RETURN: false
- TO: Sales_Agent
WHEN: intent.category == "new_booking"
CONTEXT:
pass: [search_context, preferences]
summary: "User looking to book"
EXPECT_RETURN: false
ESCALATE:
triggers:
- WHEN: routing_failures >= 3
REASON: "Multiple routing failures"
PRIORITY: high
ON_ERROR:
routing_failure:
RESPOND: "Let me connect you with someone who can help."
RETRY: 1
THEN: HANDOFF Live_Agent
COMPLETE:
- WHEN: handoff_successful == true
RESPOND: "I've connected you with the right specialist."
```
***
**Related articles:**
* [NLU](/agent-platform/abl-reference/nlu) -- intent classification that drives routing decisions
* [Memory and Constraints](/agent-platform/abl-reference/memory-and-constraints) -- session and persistent state used in routing conditions
* [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) -- condition syntax for WHEN clauses
* [Lifecycle and hooks](/agent-platform/abl-reference/lifecycle-and-hooks) -- ON\_START and hooks that interact with multi-agent flows
* [Rich content](/agent-platform/abl-reference/rich-content) -- formatting for RESPOND and COMPLETE messages
# NLU (Natural Language Understanding)
Source: https://koreai.mintlify.app/agent-platform/abl/reference/nlu
The `NLU:` block configures how the agent classifies user intent, extracts entities, resolves synonyms, and matches utterances. NLU definitions are used by the runtime for intent routing, digression matching, and entity extraction during conversation.
## Overview
ABL's NLU configuration supports:
* **Intent classification** with keyword patterns and example utterances.
* **Entity extraction** with typed extractors (enum, pattern, location, date, number, free text).
* **Synonyms** for normalizing variant expressions to canonical values.
* **Embeddings-based matching** for semantic similarity when keyword patterns are insufficient.
* **Multi-language support** with per-language model configuration.
* **A glossary** for domain-specific terminology.
```yaml expandable=true theme={null}
NLU:
intents:
- NAME: send_wire
PATTERNS: ["wire transfer", "send money", "wire funds"]
EXAMPLES: ["I need to wire $50,000 to Germany", "Can I send a domestic wire?"]
entities:
- NAME: currency_code
TYPE: enum
VALUES: [USD, EUR, GBP, JPY]
SYNONYMS:
USD: [dollars, usd, bucks]
EUR: [euros, eur]
glossary:
- "SWIFT/BIC -- Code identifying a bank globally"
- "Fedwire -- Federal Reserve real-time settlement system"
```
## Intent classification
Intents represent categories of user messages. Each intent has a name, keyword patterns for quick matching, and optional example utterances for model-based classification.
### Syntax
```yaml theme={null}
NLU:
intents:
- NAME: book_flight
PATTERNS: ["book flight", "find flights", "search flights", "fly to"]
EXAMPLES:
- "I want to fly to Paris next Tuesday"
- "Find me a round trip to London"
- "Book two seats on the morning flight to NYC"
ENTITIES: [destination, travel_date, passenger_count]
```
### Intent properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `NAME` | `string` | Yes | -- | Unique intent identifier. |
| `PATTERNS` | `string[]` | Yes | -- | Keyword patterns for quick substring matching. |
| `EXAMPLES` | `string[]` | No | -- | Full example utterances for model-based classification. Improves accuracy. |
| `EXAMPLES_FILE` | `string` | No | -- | Path to an external file containing example utterances (one per line). |
| `ENTITIES` | `string[]` | No | -- | Entity types expected to co-occur with this intent. |
### Pattern matching
Patterns are matched as case-insensitive substrings against the user's message. If any pattern appears in the message, the intent is a candidate match. Pattern matching is the first tier of classification; it is fast but imprecise.
### Example-based classification
When `EXAMPLES` are provided, the runtime uses an LLM or embedding model to classify messages based on semantic similarity to the examples. This is more accurate than pattern matching but requires more compute.
### Intent with external examples file
For intents with many examples, reference an external file:
```yaml theme={null}
NLU:
intents:
- NAME: product_inquiry
PATTERNS: ["tell me about", "what is", "describe"]
EXAMPLES_FILE: "./nlu/product_inquiry_examples.txt"
```
## Categories
The `categories:` sub-block declares coarse-grained message categories matched by keyword patterns. Categories are a lighter-weight classification than full intents (no examples or entity bindings).
```yaml theme={null}
NLU:
categories:
- NAME: billing
PATTERNS: ["invoice", "charge", "refund", "payment"]
- NAME: technical
PATTERNS: ["error", "bug", "not working", "broken"]
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `NAME` | `string` | Yes | -- | Unique category identifier. |
| `PATTERNS` | `string[]` | Yes | -- | Keyword patterns for matching. |
## Entity extraction
Entities are structured values extracted from user messages. ABL supports six entity types.
### Syntax
```yaml expandable=true theme={null}
NLU:
entities:
- NAME: currency_code
TYPE: enum
VALUES: [USD, EUR, GBP, JPY, CHF, CAD, AUD]
SYNONYMS:
USD: [dollars, usd, bucks, us dollars]
EUR: [euros, eur]
GBP: [pounds, sterling, gbp, quid]
- NAME: phone_number
TYPE: pattern
PATTERN: "\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b"
VALIDATION: "Must be a valid US phone number"
- NAME: destination
TYPE: location
- NAME: travel_date
TYPE: date
- NAME: passenger_count
TYPE: number
- NAME: special_request
TYPE: free_text
```
### Entity types
| Type | Description | Additional properties |
| - | - | - |
| `enum` | Matches against a fixed set of values with optional synonyms. | `VALUES`, `SYNONYMS` |
| `pattern` | Matches against a regular expression. | `PATTERN`, `VALIDATION` |
| `location` | Extracts geographic locations (cities, countries, addresses). | -- |
| `date` | Extracts date and time references, including relative expressions ("next Tuesday"). | -- |
| `number` | Extracts numeric values. | -- |
| `free_text` | Captures arbitrary text spans. Useful for notes, descriptions, or comments. | -- |
### Entity properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `NAME` | `string` | Yes | -- | Unique entity identifier. |
| `TYPE` | `string` | Yes | -- | Entity type. See [Entity types](#entity-types). |
| `VALUES` | `string[]` | No | -- | Valid values (for `enum` type). |
| `SYNONYMS` | `Record` | No | -- | Synonym mappings. Keys are canonical values. |
| `PATTERN` | `string` | No | -- | Regular expression (for `pattern` type). |
| `VALIDATION` | `string` | No | -- | Validation description or expression. |
| `SENSITIVE` | `boolean` | No | `false` | Marks extracted values as PII for privacy-aware handling. |
### Synonyms
Synonyms map variant expressions to canonical values. When a synonym is detected, the runtime normalizes it to the canonical form before storing:
```yaml theme={null}
SYNONYMS:
USD: [dollars, usd, bucks, us dollars]
EUR: [euros, eur]
GBP: [pounds, sterling, gbp, quid]
```
If the user says "100 bucks", the entity extraction yields `currency_code: "USD"`.
## Embeddings-based matching
For more accurate semantic matching, enable embeddings-based NLU. This uses vector similarity to match user messages against intent examples.
```yaml theme={null}
NLU:
embeddings:
enabled: true
provider: "bge-m3"
model: "BAAI/bge-m3"
threshold: 0.75
cache_ttl: 3600
```
### Embeddings properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `enabled` | `boolean` | Yes | -- | Whether embeddings-based matching is active. |
| `provider` | `string` | No | -- | Embedding model provider name. |
| `model` | `string` | No | -- | Specific model identifier. |
| `base_url` | `string` | No | -- | Custom endpoint URL for the embedding service. |
| `threshold` | `number` | No | -- | Similarity threshold (0.0--1.0). Below this, the match is rejected. |
| `cache_ttl` | `number` | No | -- | Cache duration in seconds for computed embeddings. |
## Multi-language support
ABL NLU supports multiple languages with per-language model configuration and optional code-switching detection.
```yaml theme={null}
NLU:
languages: [en, es, fr]
default_language: en
allow_code_switching: true
language_models:
en: "gpt-4o"
es: "gpt-4o"
fr: "gpt-4o"
```
### Multi-language properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `languages` | `string[]` | No | -- | Supported language codes. |
| `default_language` | `string` | No | -- | Default language when detection is ambiguous. |
| `allow_code_switching` | `boolean` | No | `false` | Allow users to switch languages mid-conversation. |
| `language_models` | `Record` | No | -- | Per-language model assignments for classification. |
## Model configuration
Specify which models to use for NLU classification tasks:
```yaml theme={null}
NLU:
models:
fast: "claude-haiku-4-5-20251001"
balanced: "claude-sonnet-4-5-20250929"
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `fast` | `string` | No | -- | Model for fast, low-latency classification. |
| `balanced` | `string` | No | -- | Model for higher-accuracy classification. |
## Evaluation configuration
Control NLU evaluation behavior for monitoring and testing:
```yaml theme={null}
NLU:
evaluation:
log_predictions: true
ab_test: true
confidence_threshold: 0.6
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `log_predictions` | `boolean` | No | -- | Log all NLU predictions for analysis. |
| `ab_test` | `boolean` | No | -- | Enable A/B testing between NLU models. |
| `confidence_threshold` | `number` | No | -- | Minimum confidence score for accepting a prediction. |
## Glossary
The glossary defines domain-specific terms and abbreviations. These definitions are injected into the LLM context to improve understanding of specialized vocabulary.
```yaml theme={null}
NLU:
glossary:
- "SWIFT/BIC -- Society for Worldwide Interbank Financial Telecommunication code"
- "IBAN -- International Bank Account Number"
- "Fedwire -- Federal Reserve real-time gross settlement system"
- "OFAC -- Office of Foreign Assets Control"
- "Beneficiary -- The person or entity receiving the wire transfer"
```
Each glossary entry is a plain string in `"term -- definition"` format.
## External NLU configuration
For complex NLU setups, reference an external configuration file:
```yaml theme={null}
NLU:
config_file: "./nlu/config.yaml"
```
The external file is merged with any inline NLU configuration. Inline values take precedence.
## Complete example
```yaml expandable=true theme={null}
NLU:
models:
fast: "claude-haiku-4-5-20251001"
balanced: "claude-sonnet-4-5-20250929"
languages: [en, es]
default_language: en
allow_code_switching: true
intents:
- NAME: send_wire
PATTERNS: ["wire transfer", "send money", "wire funds", "remittance"]
EXAMPLES:
- "I need to wire $50,000 to a vendor in Germany"
- "Can I send a domestic wire from my business account?"
- NAME: check_status
PATTERNS: ["wire status", "where is my wire", "tracking"]
EXAMPLES:
- "What's the status of confirmation WR-2024-88431?"
- NAME: cancel_wire
PATTERNS: ["cancel the wire", "stop the transfer", "recall"]
entities:
- NAME: currency_code
TYPE: enum
VALUES: [USD, EUR, GBP, JPY, CHF, CAD, AUD]
SYNONYMS:
USD: [dollars, usd, bucks]
EUR: [euros, eur]
GBP: [pounds, sterling, quid]
- NAME: transfer_type
TYPE: enum
VALUES: [domestic, international]
SYNONYMS:
domestic: [within the us, fedwire, local wire]
international: [overseas, swift, cross-border]
embeddings:
enabled: true
provider: "bge-m3"
threshold: 0.75
cache_ttl: 3600
glossary:
- "SWIFT/BIC -- Code identifying a bank globally"
- "Fedwire -- Federal Reserve real-time settlement system"
- "OFAC -- Office of Foreign Assets Control"
```
## Named entities (`ENTITIES:` section)
Separate from the `NLU:` block, the top-level `ENTITIES:` section defines **reusable** semantic entities that are shared by both NLU (for recognition) and [GATHER](/agent-platform/abl-reference/gather) (via `entity_ref`). Declaring an entity once lets multiple fields and agents reuse the same type, allowed values, synonyms, and validation.
```yaml expandable=true theme={null}
ENTITIES:
request_type:
TYPE: enum
VALUES: [REQ_REFUND, REQ_EXCHANGE, REQ_CANCEL]
SYNONYMS:
REQ_REFUND: [refund, money back, reimburse]
REQ_EXCHANGE: [exchange, swap]
booking_ref:
TYPE: pattern
PATTERN: "^[A-Z]{2}[0-9]{4,6}$"
VALIDATION: "Two letters followed by 4-6 digits"
SENSITIVE: false
```
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `TYPE` | `string` | No | `"string"` | One of the 15 unified types (see below). |
| `VALUES` | `string[]` | No | -- | Canonical values (for `enum`). |
| `SYNONYMS` | `Record` | No | -- | Canonical value → alternative forms. |
| `PATTERN` | `string` | No | -- | Regex (for `pattern`). |
| `VALIDATION` | `string` | No | -- | Intrinsic validation expression/description. |
| `SENSITIVE` | `boolean` | No | `false` | Marks the value as PII. |
| `DESCRIPTION` | `string` | No | -- | Semantic, model-facing description of what the entity value represents. A GATHER field's `entity_ref` inherits it. |
| `EXTRACTION_HINTS` | `string[]` | No | -- | Example surface forms that guide extraction. Inherited by GATHER fields that reference the entity. |
Top-level entities accept the full unified type set: `string`, `text`, `free_text`, `number`,
`integer`, `float`, `currency`, `boolean`, `date`, `datetime`, `email`, `phone`, `enum`, `pattern`,
`location`.
This is a distinct section from `NLU: entities:`. Top-level `ENTITIES:` defaults `TYPE` to `string` and supports all 15 types, whereas the `NLU: entities:` sub-block defaults to `enum` and supports only 6 types (`enum`, `pattern`, `location`, `date`, `number`, `free_text`). A GATHER field's `entity_ref` points at a top-level `ENTITIES:` entry.
## Intent categories (`INTENTS:` section)
The top-level `INTENTS:` section declares supervisor routing categories (distinct from `NLU: intents:`). Each entry is a category name with an optional description, and the block can set a lexical-fallback mode.
```yaml theme={null}
INTENTS:
LEXICAL_FALLBACK: when_unavailable
new_booking: "Customer wants to book new travel"
manage_booking: "Change or cancel an existing booking"
billing
```
| Property | Type | Description |
| - | - | - |
| category name | `string` | Intent category identifier; optionally followed by `: "description"`. |
| `LEXICAL_FALLBACK` | `string` | When to fall back to lexical matching: `never`, `when_unavailable`, or `always`. |
## Multi-intent handling (`MULTI_INTENT:`)
The top-level `MULTI_INTENT:` section configures how the agent handles a single user message that contains **more than one intent** — for example, *"Book me a flight to NYC and also cancel my hotel reservation."* When enabled, the NLU pipeline detects and triages multiple intents per turn, and the `strategy` controls how they are dispatched.
```yaml theme={null}
MULTI_INTENT:
strategy: primary_queue
unknown_relationship_strategy: sequential
max_intents: 5
confidence_threshold: 0.7
queue_max_age_ms: 300000
enabled: true
```
### Properties
| Property | Type | Default | Description |
| - | - | - | - |
| `enabled` | `boolean` | `true` | Whether multi-intent detection and triage is active. |
| `strategy` | `string` | `primary_queue` | How detected intents are dispatched. See [Strategies](#multi-intent-strategies). |
| `unknown_relationship_strategy` | `string` | `parallel` | Fallback strategy when the relationship between intents is unknown: `sequential`, `parallel`, or `disambiguate`. |
| `max_intents` | `number` | `3` | Maximum intents to consider per message (1–10). |
| `confidence_threshold` | `number` | `0.6` | Minimum confidence (0.0–1.0) for a secondary intent to be accepted. |
| `queue_max_age_ms` | `number` | `600000` | Time-to-live for queued intents in milliseconds (0–3,600,000). After this, queued intents are dropped. |
| `advance_on_primary_response` | `boolean` | `true` | With `strategy: primary_queue`, whether the primary specialist's first delivered, non-blocking answer counts as done for queue purposes — advancing to the next queued intent in the same turn. Also applies to a `sequential` task resuming after a nested child returns, and to the option picked from a `disambiguate` menu. Set `false` to require an explicit completion signal (a `COMPLETE` condition, a terminal control, or return-to-parent) before advancing. See the note below for how a prose-only reply is judged. |
### Multi-intent strategies
| Strategy | Behavior |
| - | - |
| `primary_queue` | Handle the highest-confidence intent immediately; queue the rest and surface them one-by-one after it completes. (Default.) |
| `sequential` | Execute the detected intents one at a time in order, carrying context forward between them. |
| `parallel` | Fan out to sub-agents simultaneously (supervisor agents only). |
| `disambiguate` | Present the detected intents to the user and ask which to handle first. |
| `auto` | Resolves at runtime from the assessed relationship between the intents: independent and dependent requests resolve to `sequential`, and ambiguous ones resolve to `disambiguate`. It never fans out — author `strategy: parallel` directly for that. |
The user-facing prompts for the `disambiguate` and `primary_queue` flows (the "I noticed more than one request…" text) are customizable via the [`MESSAGES:`](/agent-platform/abl-reference/lifecycle-and-hooks#messages) block (keys such as `multi_intent_disambiguate_header` and `multi_intent_queued_notice`).
A specialist with a missing required `GATHER` field, a pending input/tool confirmation, an authentication step, an escalation, or a still-waiting child keeps the floor regardless of `advance_on_primary_response`. A request whose tool/model call errored (and the customer was told) counts as finished, so the plan moves on. For a reply given only in prose (no `GATHER`) — the ambiguous case — the Runtime judges the reply itself: one that still waits on the customer (an unresolved follow-up question) keeps the floor, a finished-sounding one advances, and one the Runtime can't judge keeps the floor. Set `advance_on_primary_response: false` to keep the specialist in control regardless of what it replies.
In a `sequential` plan, the last request's specialist keeps the customer's next reply when it answers without finishing (no `COMPLETE`, no return) — the same as after a single routed request — and may hand that reply back with `__return_to_parent__` when the supervisor's handoff to it declared `RETURN: true`.
***
**Related articles:**
* [Multi-Agent and Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) -- intent-based routing in Supervisors
* [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) -- expression syntax used in intent conditions
* [Memory and Constraints](/agent-platform/abl-reference/memory-and-constraints) -- NLU results stored in session variables
# Rich content
Source: https://koreai.mintlify.app/agent-platform/abl/reference/rich-content
This page documents ABL's rich content output formats: voice configuration, format-specific content (Markdown, Adaptive Cards, HTML, Slack, WhatsApp), carousels, interactive actions, feedback surveys, and reusable templates. For the expression language (operators, functions, `{{}}` interpolation syntax), see [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions).
***
## Rich content
ABL supports multi-format output for delivering responses across different channels (web, mobile, voice, messaging platforms). The `VOICE:`, `RICH_CONTENT:`, and `ACTIONS:` blocks can be attached to any `RESPOND` statement, `COMPLETE` condition, or lifecycle handler. A pure reasoning agent (no `FLOW:`) may also declare top-level `RICH_CONTENT:` and `ACTIONS:` blocks that attach to its generated responses.
### Overview
A single response can include:
* **Plain text** -- the default `RESPOND` string.
* **Voice configuration** -- SSML markup or natural language voice instructions.
* **Rich content** -- Markdown, Adaptive Cards, HTML, Slack Block Kit, WhatsApp, or AG-UI.
* **Carousels** -- scrollable card collections with images and buttons.
* **Interactive actions** -- buttons, select menus, and input fields.
* **Templates** -- reusable named response definitions with interpolation.
The runtime selects the appropriate format based on the delivery channel.
### Voice configuration
Voice configuration provides channel-specific voice output. The `VOICE:` block can appear alongside any `RESPOND`.
#### Syntax
```yaml theme={null}
RESPOND: "Your booking is confirmed for December 15th."
VOICE:
ssml: |
Your booking is confirmed for 12/15/2025 .
instructions: "Speak in a warm, congratulatory tone"
plain_text: "Your booking is confirmed for December fifteenth."
```
#### Voice properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `provider` | `string` | No | -- | TTS provider (for example. `elevenlabs`, `google`, `azure`, `openai`). |
| `voice_id` | `string` | No | -- | Provider-specific voice identifier. |
| `speed` | `number` | No | `1.0` | Speech-rate multiplier (`1.0` = normal). |
| `ssml` | `string` | No | -- | W3C SSML markup for TTS engines (Google, Azure, Amazon Polly). |
| `instructions` | `string` | No | -- | Natural language voice style instructions (OpenAI Realtime, Gemini Live). |
| `plain_text` | `string` | No | -- | Voice-optimized plaintext. Used by ElevenLabs and as a fallback for all engines. |
| `pace` | `number` | No | -- | Numeric speaking-rate multiplier (authoring alias for speed/pace controls). |
| `temperature` | `number` | No | -- | Provider-specific TTS sampling/variation control. |
| `auto_tts_language_switch` | `boolean` | No | -- | Switch the TTS voice/model to match the detected language of the response. |
#### SSML example
```yaml theme={null}
VOICE:
ssml: |
Your wire transfer of $50,000 USD
has been executed.
The confirmation number is
WR-2024-88431 .
```
#### Natural language instructions
For voice platforms that accept style instructions rather than SSML:
```yaml theme={null}
VOICE:
instructions: "Speak slowly and clearly, emphasizing the confirmation number. Use a professional but warm tone."
```
### Rich content formats
The rich-content block provides format-specific variants of a response. The runtime selects the variant matching the delivery channel.
`RICH_CONTENT:` is the canonical block keyword; `FORMATS:` is the original spelling, kept as an accepted alias. The `.agent.yaml` format accepts `rich_content:` or `formats:` the same way. Most places a rich-content block is authored — directly under a `RESPOND:` (in a flow step, `ON_START`, `HOOKS:`, or an `ON_ACTION`/`ON_ERROR` handler), in a tool's `on_result:`/`on_error:` action block, and in a `COMPLETE:` condition (including one with no `RESPOND:` at all) — accept either spelling interchangeably.
#### Syntax
```yaml expandable=true theme={null}
RESPOND: "Here are your flight options."
FORMATS:
MARKDOWN: |
## Flight Options
| Flight | Departure | Arrival | Price |
|--------|-----------|---------|-------|
| AA 142 | 8:00 AM | 11:30 AM | $349 |
| UA 891 | 10:15 AM | 1:45 PM | $289 |
ADAPTIVE_CARD: |
{
"type": "AdaptiveCard",
"body": [
{"type": "TextBlock", "text": "Flight Options", "size": "large"}
]
}
HTML: |
SLACK: |
{
"blocks": [
{"type": "header", "text": {"type": "plain_text", "text": "Flight Options"}}
]
}
```
#### Rich content properties
Rich content supports two kinds of properties. Channel-specific string formats carry a payload written for one channel. Structured template formats define content once, and the runtime renders it for each channel.
**Channel-specific string formats:**
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `MARKDOWN` | `string` | No | -- | Formatted markdown text. |
| `ADAPTIVE_CARD` | `string` | No | -- | JSON string conforming to the Microsoft Adaptive Cards schema. |
| `HTML` | `string` | No | -- | HTML content for web-based channels. |
| `SLACK` | `string` | No | -- | JSON string conforming to the Slack Block Kit format. |
| `AG_UI` | `string` | No | -- | JSON string for AG-UI / CopilotKit events. |
| `WHATSAPP` | `string` | No | -- | JSON string for WhatsApp interactive messages. |
**Structured template formats** (channel-neutral; the runtime renders them per channel):
Structured templates describe content as data, such as a carousel, table, or chart. The runtime converts each template to the native format of the target channel. Templates fall into two groups: *basic templates* for simple display, and *data-rich templates* for structured data.
*Basic templates* — display-only, no aggregation or layout options beyond their own items:
| Property | Type | Description |
| - | - | - |
| `CAROUSEL` | `object` | Scrollable card collection. See [Carousels](#carousels). |
| `QUICK_REPLIES` | `array` | Quick-reply chips: each `{ id, label, iconUrl? }`. See [Quick replies](#quick-replies). |
| `LIST` | `object` | `{ title?, items: [{ title, subtitle?, imageUrl?, defaultActionUrl? }] }`. See [Lists](#lists). |
| `IMAGE` | `object` | `{ url, alt?, thumbnailUrl?, caption? }`. See [Media](#media-image-video-audio). |
| `VIDEO` | `object` | `{ url, alt?, thumbnailUrl?, caption? }`. See [Media](#media-image-video-audio). |
| `AUDIO` | `object` | `{ url, alt?, thumbnailUrl?, caption? }`. See [Media](#media-image-video-audio). |
| `FILE` | `object` | `{ url, filename, sizeBytes?, mimeType? }`. See [Files](#files). |
*Data-rich templates* — designed to present structured/tabular data, with extra options like `adaptive` narrowing:
| Property | Type | Description |
| - | - | - |
| `KPI` | `object` | `{ label, value, unit?, trend?: up/down/flat, iconUrl? }`. See [KPIs](#kpis). |
| `TABLE` | `object` | `{ columns: [{ key, header, align?: left/center/right }], rows, maxVisibleRows? }`. See [Tables](#tables). |
| `CHART` | `object` | `{ type: bar/line/pie, title?, data: [{ label, value, color? }] }`. See [Charts](#charts). |
| `FORM` | `object` | `{ title?, fields: [ActionElement], submitLabel? }`. See [Forms](#forms). |
| `PROGRESS` | `object` | `{ label?, value, max?, variant?: bar/circle }`. See [Progress](#progress). |
| `FEEDBACK` | `object` | Rating/feedback survey. `{ FEEDBACK_TEMPLATE?, prompt, type: thumbs/star/scale, max?, submit_label?, pending_message?, success_message?, error_message?, comment_threshold?, comment_prompt?, comment_placeholder?, when?, _rules? }`. See [FEEDBACK surveys](#feedback-surveys). |
Named templates (in the `TEMPLATES:` block) may also declare a `RENDERABLES:` list — customer-owned structured payloads with `name`, `payload`, optional `targets` (`api`/`sdk_websocket`/`http_async`), `fallbackText`, and `schemaRef`. Several structured templates also support **collection binding** — `from:`/`template:` — to render one entry per array item; see [Collection binding](#collection-binding) for exactly which ones.
#### Template modifiers
Two cross-cutting modifiers apply to structured templates:
* **`when`** — a conditional render guard (expression). On an agent-level rich-content block, the template renders only when the expression is truthy at reasoning finalization. Available on the rich-content block and on each structured template. See [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) for expression syntax.
* **`adaptive`** — on `LIST`, `KPI`, and `TABLE` templates whose items/rows are bound to a tool variable, set `adaptive: true` to let the reasoning agent narrow which items/rows/columns render for the user's request.
#### Collection binding
Several structured templates have an array-valued property — `CAROUSEL.cards`, `QUICK_REPLIES` itself, `LIST.items`, `TABLE.columns` and `TABLE.rows`, `CHART.data`, and `FORM.fields`. Each of these accepts **three** forms:
1. **A literal array** — write the items directly, as shown in every static example on this page.
2. **A `from`/`template` binding** — resolve the array from a session variable at render time, and render one entry per array item:
```yaml theme={null}
data:
from: "{{tool_result.output.sectors}}" # or bare, without {{ }}: tool_result.output.sectors
template:
label: "{{name}}"
value: "{{percentage}}"
```
`from` names the session/tool-result path holding the array (the `{{ }}` wrapper is optional — both forms resolve identically). `template` is applied once per array item; inside it, `{{ }}` placeholders resolve against that item's own fields merged over the outer session data — so an item shaped `{name: "Equities", percentage: 45}` is referenced as `{{name}}`/`{{percentage}}`, **not** `{{item.name}}`. If an array element isn't an object (a bare number or string), it's exposed as `{{value}}` instead.
3. **A bare string reference** — when the tool result is *already* shaped exactly like the target items (for example, a tool that already returns `{label, value, color}` objects for a chart), skip `template:` entirely:
```yaml theme={null}
data: "{{tool_result.output.pre_shaped_points}}"
```
`template` is optional within a `from` binding — a binding with only `from` behaves like the bare-string form above, applied to the resolved array. `options` on a `select`-type action element (used inside `FORM.fields` and `ACTIONS`) is itself collection-bound the same way, so a select's options list can also be tool-driven.
### Carousels
Carousels display a horizontal scrollable collection of cards, each with a title, subtitle, image, and action buttons.
#### Syntax
```yaml expandable=true theme={null}
FORMATS:
CAROUSEL:
- title: "Economy Class"
subtitle: "$289 - UA 891"
imageUrl: "https://cdn.example.com/economy.jpg"
defaultActionUrl: "https://booking.example.com/ua891"
buttons:
- id: select_economy
type: button
label: "Select"
value: "ua891_economy"
- title: "Business Class"
subtitle: "$1,249 - UA 891"
imageUrl: "https://cdn.example.com/business.jpg"
buttons:
- id: select_business
type: button
label: "Select"
value: "ua891_business"
```
#### Carousel card properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `title` | `string` | Yes | -- | Card title. |
| `subtitle` | `string` | No | -- | Card subtitle or description. |
| `imageUrl` | `string` | No | -- | URL for the card image. |
| `defaultActionUrl` | `string` | No | -- | URL opened when the card itself is tapped. |
| `buttons` | `array` | No | -- | Action buttons. See [Interactive actions](#interactive-actions). |
`CAROUSEL` also accepts a `when` render guard, and `cards` supports [collection binding](#collection-binding) — resolve the cards from a tool result instead of writing them statically:
```yaml theme={null}
FORMATS:
CAROUSEL:
when: hotel_results.length > 0
from: "{{search_result.output.hotels}}"
template:
title: "{{name}}"
subtitle: "{{price_display}}"
imageUrl: "{{photo_url}}"
```
### Quick replies
Quick-reply chips give the user a small set of one-tap responses.
#### Syntax
```yaml theme={null}
RESPOND: "Would you like to proceed?"
FORMATS:
QUICK_REPLIES:
- id: yes
label: "Yes, proceed"
- id: no
label: "No, cancel"
```
#### Quick reply properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `id` | `string` | Yes | -- | Value posted back when the chip is tapped. |
| `label` | `string` | Yes | -- | Chip display text. |
| `iconUrl` | `string` | No | -- | Optional icon shown on the chip. |
`QUICK_REPLIES` itself supports [collection binding](#collection-binding) — the whole list can come from `from`/`template` — but, unlike the other structured templates, it has **no `when` guard**: the property is the raw collection, with no wrapping object to hang a render guard on.
### Lists
A vertical list of items, each with an optional subtitle, image, and tap action.
#### Syntax
```yaml theme={null}
FORMATS:
LIST:
title: "Nearby Branches"
items:
- title: "Downtown Branch"
subtitle: "0.4 miles away — open until 6 PM"
imageUrl: "https://cdn.example.com/downtown.jpg"
defaultActionUrl: "https://maps.example.com/downtown"
- title: "Uptown Branch"
subtitle: "1.2 miles away — open until 5 PM"
```
#### List properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `adaptive` | `boolean` | No | `false` | Let the reasoning agent narrow which items render. See [Template modifiers](#template-modifiers). |
| `title` | `string` | No | -- | List heading. |
| `items` | `array` | Yes | -- | List items. Supports [collection binding](#collection-binding). |
#### List item properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `title` | `string` | Yes | -- | Item title. |
| `subtitle` | `string` | No | -- | Item subtitle or description. |
| `imageUrl` | `string` | No | -- | URL for the item's image. |
| `defaultActionUrl` | `string` | No | -- | URL opened when the item itself is tapped. |
Dynamic example — one list item per branch a tool returns:
```yaml theme={null}
FORMATS:
LIST:
title: "Nearby Branches"
items:
from: "{{search_result.output.branches}}"
template:
title: "{{branch_name}}"
subtitle: "{{distance_label}}"
```
### Media (image, video, audio)
`IMAGE`, `VIDEO`, and `AUDIO` share an identical shape — a single media item, not a collection.
#### Syntax
```yaml theme={null}
FORMATS:
IMAGE:
url: "https://cdn.example.com/receipt-12345.png"
alt: "Payment receipt"
caption: "Your receipt for confirmation #12345"
```
#### Media properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `url` | `string` | Yes | -- | Media URL. |
| `alt` | `string` | No | -- | Accessibility text (also used as a text fallback where media can't render). |
| `thumbnailUrl` | `string` | No | -- | Preview thumbnail, mainly for `VIDEO`/`AUDIO`. |
| `caption` | `string` | No | -- | Caption shown alongside the media. |
`IMAGE`/`VIDEO`/`AUDIO` are each a single object — there's no array, so [collection binding](#collection-binding) doesn't apply here. To show several images, use a `LIST` or `CAROUSEL` instead.
### Files
A downloadable file attachment.
#### Syntax
```yaml theme={null}
FORMATS:
FILE:
url: "https://cdn.example.com/statements/2026-08.pdf"
filename: "August_2026_Statement.pdf"
sizeBytes: 184320
mimeType: "application/pdf"
```
#### File properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `url` | `string` | Yes | -- | File URL. |
| `filename` | `string` | Yes | -- | Display filename. |
| `sizeBytes` | `number` | No | -- | File size in bytes, shown to the user. |
| `mimeType` | `string` | No | -- | MIME type, used to pick a file-type icon. |
### KPIs
A single metric tile — a label, a value, and an optional trend indicator.
#### Syntax
```yaml theme={null}
FORMATS:
KPI:
label: "Account Balance"
value: 12450.75
unit: "USD"
trend: up
```
#### KPI properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `adaptive` | `boolean` | No | `false` | Let the reasoning agent select this KPI among other `adaptive` templates. See [Template modifiers](#template-modifiers). |
| `label` | `string` | Yes | -- | Metric label. |
| `value` | `string` \| `number` | Yes | -- | Metric value. |
| `unit` | `string` | No | -- | Unit suffix (for example `"USD"`, `"%"`). |
| `trend` | `"up"` \| `"down"` \| `"flat"` | No | -- | Trend indicator. |
| `iconUrl` | `string` | No | -- | Icon shown on the tile. |
`KPI` is a **single value**, not an array — there's no [collection binding](#collection-binding) here (that's what distinguishes it from `TABLE`/`LIST`/`CHART`). To show multiple KPIs, author multiple `KPI` blocks or use a `TABLE`.
### Tables
Columns and rows for tabular data.
#### Syntax
```yaml expandable=true theme={null}
FORMATS:
TABLE:
columns:
- key: date
header: "Date"
- key: description
header: "Description"
- key: amount
header: "Amount"
align: right
rows:
- date: "2026-09-01"
description: "Direct deposit"
amount: "$2,450.00"
- date: "2026-09-03"
description: "Grocery store"
amount: "-$86.20"
maxVisibleRows: 10
```
#### Table properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `adaptive` | `boolean` | No | `false` | Let the reasoning agent narrow which rows/columns render. See [Template modifiers](#template-modifiers). |
| `columns` | `array` | Yes | -- | Column definitions. Supports [collection binding](#collection-binding). |
| `rows` | `array` | Yes | -- | Row data — each row an object keyed by column `key`. Supports [collection binding](#collection-binding), independently of `columns`. |
| `maxVisibleRows` | `number` | No | -- | Rows shown before the client truncates/paginates. |
#### Table column properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `key` | `string` | Yes | -- | Row property this column reads. |
| `header` | `string` | Yes | -- | Column header text. |
| `align` | `"left"` \| `"center"` \| `"right"` | No | `left` | Column text alignment. |
Dynamic example — columns declared statically, rows bound to a tool result (the two bindings are independent, so it's common to declare `columns` statically and bind only `rows`):
```yaml expandable=true theme={null}
FORMATS:
TABLE:
columns:
- key: date
header: "Date"
- key: description
header: "Description"
- key: amount
header: "Amount"
align: right
rows:
from: "{{transactions_result.output.items}}"
template:
date: "{{posted_date}}"
description: "{{merchant_name}}"
amount: "{{formatted_amount}}"
```
### Charts
Pie, bar, or line charts, rendered as SVG on channels that support them.
Currently, charts render on the Web SDK only. Color is optional — the SDK provides a default palette for dynamic charts.
#### Syntax
```yaml expandable=true theme={null}
RESPOND: "Here is the allocation:"
FORMATS:
CHART:
type: pie
title: "Portfolio Allocation"
data:
- label: "Equities"
value: 45
color: "#4f46e5"
- label: "Bonds"
value: 25
color: "#06b6d4"
- label: "Cash"
value: 10
color: "#10b981"
```
#### Chart properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `type` | `"pie"` \| `"bar"` \| `"line"` | Yes | -- | Chart type. |
| `title` | `string` | No | -- | Chart title. |
| `data` | `array` | Yes | -- | Data points. Supports [collection binding](#collection-binding). |
#### Chart data point properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `label` | `string` | Yes | -- | Data point label (category / x-axis value). |
| `value` | `string` \| `number` | Yes | -- | Data point value. |
| `color` | `string` | No | -- | Optional hex color for this data point/slice. |
Dynamic example — one data point per array item a tool returns:
```yaml theme={null}
RESPOND: "Portfolio for {{investor_id}}:"
FORMATS:
CHART:
type: pie
title: "Allocation — {{investor_id}}"
data:
from: "{{tool_result.output.sectors}}"
template:
label: "{{name}}"
value: "{{percentage}}"
```
### Interactive actions
Interactive actions add buttons, select menus, and input fields to a response. Users interact with these elements, and the agent handles the interactions via `ON_ACTION` blocks.
#### Syntax
```yaml expandable=true theme={null}
RESPOND: "How would you like to proceed?"
ACTIONS:
- id: confirm_wire
type: button
label: "Confirm Wire"
value: "confirmed"
- id: modify_amount
type: button
label: "Modify Amount"
value: "modify"
- id: cancel
type: button
label: "Cancel"
value: "cancelled"
```
#### Action element properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `id` | `string` | Yes | -- | Unique action identifier. Referenced in `ON_ACTION` handlers. |
| `type` | `string` | Yes | -- | Element type: `button`, `select`, or `input`. |
| `label` | `string` | Yes | -- | Display label. |
| `value` | `string` | No | -- | Hidden value sent when the user interacts with this element. |
| `url` | `string` | No | -- | For `button` type only: clicking opens this link instead of posting the value back to the agent. (Aliases: `link`, `web_url`.) |
| `description` | `string` | No | -- | Subtitle or help text (for list items). |
| `options` | `array` | No | -- | Options for `select` type. Each has `id`, `label`, `description`. |
| `inputType` | `string` | No | -- | Input type for `input` elements: `text`, `number`, `date`, `time`, `email`. |
| `placeholder` | `string` | No | -- | Placeholder text for `input` elements. |
| `required` | `boolean` | No | -- | Whether the input is required before submission. |
#### Select element example
```yaml theme={null}
ACTIONS:
- id: select_account
type: select
label: "Choose Account"
options:
- id: checking
label: "Checking ****4521"
description: "Available: $12,340.50"
- id: savings
label: "Savings ****8872"
description: "Available: $45,200.00"
```
#### Input element example
```yaml expandable=true theme={null}
ACTIONS:
- id: amount_input
type: input
label: "Transfer Amount"
inputType: number
placeholder: "Enter amount"
required: true
- id: reference_input
type: input
label: "Reference (optional)"
inputType: text
placeholder: "Invoice number, memo, etc."
required: false
submitLabel: "Submit"
submitId: "submit_transfer"
```
#### Form submission
When the `ACTIONS` block contains `input` elements, you can specify a `submitLabel` and `submitId` for the form submission button:
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `submitLabel` | `string` | No | -- | Label for the form submit button. |
| `submitId` | `string` | No | -- | Action ID emitted when the form is submitted. |
#### ON\_ACTION handlers
Handle user interactions with action elements in flow steps:
```yaml expandable=true theme={null}
FLOW:
review_wire:
RESPOND: "Ready to proceed?"
ACTIONS:
- id: confirm
type: button
label: "Confirm"
- id: cancel
type: button
label: "Cancel"
ON_ACTION:
confirm:
DO:
- SET: user_confirmed = true
- RESPOND: "Executing your wire transfer."
FORMATS:
MARKDOWN: "**Executing your wire transfer.**"
- GOTO: execute_wire_step
cancel:
RESPOND: "Wire transfer cancelled."
GOTO: cleanup
```
The `ON_ACTION` handler properties are described below.
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `ACTION_ID` | `string` | Yes | -- | Which action element triggered this handler. |
| `CONDITION` | `string` | No | -- | Optional condition based on the action value. |
| `DO` | ordered action list | No | -- | Preferred form when order or nested rich `FORMATS:` matters. |
| `RESPOND` | `string` | No | -- | Response message; supports nested `FORMATS:` inside a `DO` action. |
| `SET` | `Record` | No | -- | Variable assignments. |
| `CLEAR` | `string[]` | No | -- | Session variables to clear. |
| `CALL` | `string` | No | -- | Tool call; use `AS result_key` to store the result. |
| `GOTO` / `TRANSITION` / `THEN` | `string` | No | -- | Flow step to transition to. |
| `HANDOFF` | `string` | No | -- | Declared handoff target agent. |
| `DELEGATE` | `string` | No | -- | Declared delegate target agent; supports nested `RETURN` and `ON_RETURN`. |
| `COMPLETE` | `boolean` | No | `true` | Complete the current flow. |
If a handler `RESPOND` includes rich `FORMATS:` before terminal routing, that payload is forwarded as the fallback final channel payload. The terminal target's own rich payload takes precedence.
#### Reusable handlers (`ACTION_HANDLERS:`)
`ON_ACTION` inside a flow step handles actions for that step only. To define **reusable** action handlers available across the whole agent, declare a top-level `ACTION_HANDLERS:` section. Each entry is keyed by the action element `id` and uses the same grammar as `ON_ACTION` (an optional `CONDITION`, shorthand `RESPOND`/`SET`/`TRANSITION`, or a canonical `DO:` block of ordered actions — `SET`, `CLEAR`, `LOG`, `RESPOND`, `CALL`, `GOTO`/`TRANSITION`/`THEN`, `HANDOFF`, `DELEGATE`, `COMPLETE`).
```yaml expandable=true theme={null}
ACTION_HANDLERS:
open_summary:
DO:
- SET: selected_action = "open_summary"
- RESPOND: TEMPLATE(account_summary)
- TRANSITION: display_summary
contact_support:
CONDITION: user.tier == "premium"
DO:
- RESPOND: "Connecting you to premium support."
- HANDOFF: Support_Agent
```
### Forms
A form groups several [action elements](#action-element-properties) — typically `input` fields, plus a submit control — into one submission.
#### Syntax
```yaml expandable=true theme={null}
RESPOND: "Let's set up your transfer."
FORMATS:
FORM:
title: "Transfer Details"
fields:
- id: amount_input
type: input
label: "Amount"
inputType: number
required: true
- id: memo_input
type: input
label: "Memo (optional)"
inputType: text
required: false
submitLabel: "Continue"
```
#### Form properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `title` | `string` | No | -- | Form heading. |
| `fields` | `array` | Yes | -- | Form fields — the same shape as [action elements](#action-element-properties) (`id`, `type`, `label`, and the rest). Supports [collection binding](#collection-binding). |
| `submitLabel` | `string` | No | -- | Label for the form's submit button. |
`FORM` doesn't have its own `submitId` — submission is handled the same way as any other `ACTIONS` form submission; see [Form submission](#form-submission) and [ON\_ACTION handlers](#on_action-handlers).
### Progress
A progress bar or circle for a single value against a maximum.
#### Syntax
```yaml theme={null}
FORMATS:
PROGRESS:
label: "Document verification"
value: 2
max: 3
variant: bar
```
#### Progress properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `when` | `string` | No | -- | Conditional render guard. See [Template modifiers](#template-modifiers). |
| `label` | `string` | No | -- | Label shown with the progress indicator. |
| `value` | `number` | Yes | -- | Current progress value. |
| `max` | `number` | No | -- | Maximum value the progress is measured against. |
| `variant` | `"bar"` \| `"circle"` | No | `bar` | Visual style. |
`PROGRESS` is a single value, not an array — there's no [collection binding](#collection-binding) here.
### FEEDBACK surveys
A `FEEDBACK` block renders a rating/feedback card. Author it inside a `RICH_CONTENT:` (or `FORMATS:`) block.
**Inline feedback is deprecated** (`DEPRECATED_INLINE_FEEDBACK`). A `FEEDBACK` block without a `FEEDBACK_TEMPLATE` still compiles, but the compiler warns: *"Inline feedback is deprecated and will be removed in a future version. Define a survey in the project Surveys area and reference it with `FEEDBACK_TEMPLATE`."* Prefer referencing a project survey template.
#### Referencing a survey template
Set `FEEDBACK_TEMPLATE` to a project survey name. The `prompt` and `type` (the survey's **measure**) then come from the template, so they become optional here; any copy fields you do set act as **per-string overrides** (useful for translation):
```yaml theme={null}
RICH_CONTENT:
FEEDBACK:
FEEDBACK_TEMPLATE: Post_Chat_NPS
prompt: "How likely are you to recommend us?" # optional copy override
```
When a template is named you may **not** override the measure — `type`, `max`, `scoring`, `range`, and `comment_threshold` are owned by the template, and setting them alongside a template is a compile error. Naming a template that doesn't exist in the project is an error (`UNKNOWN_SURVEY_TEMPLATE`).
`expires_after` is the one exception to that rule — a numeric answer-window TTL (in seconds) that **is** overridable per-agent even when a template is referenced:
```yaml theme={null}
RICH_CONTENT:
FEEDBACK:
FEEDBACK_TEMPLATE: Post_Chat_NPS
prompt: "How likely are you to recommend us?"
expires_after: 600 # widen the answer window to 10 minutes for this agent
```
The effective window is the agent's `expires_after` if set, else the template's own TTL, else a 300-second platform default — clamped to 30–86400 seconds either way. A survey presented to the customer can only be answered within this window; a submission after it expires is rejected.
#### Inline FEEDBACK (deprecated)
Without a template, `prompt` and `type` are required:
| Field | Type | Description |
| - | - | - |
| `prompt` | `string` | The question (aliases: `question`, `label`). |
| `type` | `"thumbs"` \| `"star"` \| `"scale"` | Rating measure. `stars` is an accepted alias for `star`. |
| `max` | `number` | Maximum rating value (for `star`/`scale`). |
| `submit_label` | `string` | Submit-button label. |
| `pending_message` / `success_message` / `error_message` | `string` | Status copy. |
| `comment_prompt` / `comment_placeholder` | `string` | Free-text comment field copy. |
| `comment_threshold` | `number` | Rating at/below which the comment field is requested. |
| `when` | `string` | Conditional render guard (expression). |
#### Condition-based wording (`_rules`)
To vary wording by condition — the second way to translate a survey, kept next to the source copy — add a `_rules` list. Each rule has a `when` expression and the field's replacement text (the field key, or the alias `value`). Rules are ordered, first match wins:
```yaml theme={null}
FEEDBACK:
FEEDBACK_TEMPLATE: Post_Chat_NPS
prompt: "How likely are you to recommend us?"
prompt_rules:
- when: "language == 'es'"
prompt: "¿Nos recomendaría?" # or: value: "¿Nos recomendaría?"
comment_placeholder_rules:
- when: "language == 'es'"
value: "Cuéntenos más..."
```
Only **wording** fields accept rules: `prompt`, `submit_label`, `pending_message`, `success_message`, `error_message`, `comment_prompt`, `comment_placeholder`. There is no `type_rules`/`max_rules` — the measure is never rule-selectable.
Copy rules are evaluated **before the customer answers**, so a `when` expression may reference `max`, `language`, `channel`, and `session` (as `session.`) but **not** `rating`. An invalid condition is a compile error (`INVALID_SURVEY_CONDITION`).
#### Post-transfer surveys (ESCALATE to a human agent)
When a human agent closes a conversation the bot [escalated](/agent-platform/abl-reference/multi-agent-and-supervisor#escalate), the survey card shown on close is resolved from a `FEEDBACK` block — but that card renders with no bot turn running, so it can't come from "whichever step the conversation happened to be on." The platform instead picks **one** `FEEDBACK` block to stand for the whole agent:
1. **The agent-level block**, if the agent declares one directly under its top-level `RICH_CONTENT:` (outside any `FLOW:` step) — this always wins when present.
2. **Otherwise, a FLOW step's own `FEEDBACK` block.** Both placements compile to the same thing: `FEEDBACK` nested under a step's own `RICH_CONTENT:`, or under that step's `RESPOND:`. The platform scans steps in `FLOW:` order:
* **Exactly one step** has a `FEEDBACK` block → that one is used.
* **Several steps** have a `FEEDBACK` block, naming the **same** `FEEDBACK_TEMPLATE` → treated as the same survey; the first step's copy/wording wins.
* **Several steps** have `FEEDBACK` blocks naming **different** templates, or any of them is an inline (template-less) block → ambiguous. No post-transfer survey is shown at all, rather than guessing which one you meant.
```yaml theme={null}
FLOW:
collect_card:
REASONING: false
RESPOND: "Let me get that taken care of for you."
RICH_CONTENT:
FEEDBACK:
FEEDBACK_TEMPLATE: Post_Transfer_CSAT
THEN: transfer_to_agent
```
A `FEEDBACK` block's `when` guard is **not** evaluated for this purpose — closing the conversation isn't a turn, and `when` conditions over turn-scoped values (like `input`) would never hold outside one. A block nested deeper than the step itself — inside an `ON_ACTION` handler, a `GATHER` field, a digression, or a reasoning zone — is never a candidate; only the step's own `RICH_CONTENT:`/`RESPOND:`-level block counts.
The transfer record stays pending after the human agent finishes and only closes once the survey is answered or its answer window ([`expires_after`](#referencing-a-survey-template)) runs out — whichever happens first. The resulting rating is stored with the survey template, the escalating agent's name, and its version, so it appears correctly scoped in that survey's Feedback Insights.
### Templates
Templates are named, reusable response definitions declared in the `TEMPLATES:` block. They support `{{}}` interpolation and multi-format variants.
#### Syntax
```yaml expandable=true theme={null}
TEMPLATES:
wire_confirmation:
DEFAULT: |
**Wire Confirmation -- {{confirmation_number}}**
From: ****{{source_last4}} | To: {{beneficiary_name}}
Amount: {{amount}} {{currency}} | Fees: {{fees}} | Total: {{total_debit}}
Status: {{status}} | ETA: {{estimated_arrival}}
MARKDOWN: |
## Wire Transfer Confirmation
| Field | Value |
|-------|-------|
| Confirmation | {{confirmation_number}} |
| Amount | {{amount}} {{currency}} |
| Status | {{status}} |
fee_breakdown:
DEFAULT: |
**Fee Breakdown for {{transfer_type}} wire**
Wire fee: {{wire_fee}} {{currency}}
{{#if intermediary_fee}}Intermediary fee: {{intermediary_fee}} {{currency}}{{/if}}
**Total fees: {{total_fees}} {{currency}}**
```
#### Template properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | `string` | Yes | -- | Template name (the YAML key). Referenced via `TEMPLATE(name)`. |
| `DEFAULT` | `string` | Yes | -- | Default template body with `{{}}` interpolation. |
| `MARKDOWN` | `string` | No | -- | Markdown variant. |
| `ADAPTIVE_CARD` | `string` | No | -- | Adaptive Card JSON variant. |
| `ACTIONS` | `object` | No | -- | Interactive actions attached to this template. |
| `VOICE` | `object` | No | -- | Voice (TTS) overrides for this template. Same shape as [Voice configuration](#voice-configuration). |
| `RENDERABLES` | `object[]` | No | -- | Customer-owned structured payload contracts (`name`, `payload`, `targets`, `fallbackText`, `schemaRef`). |
#### Referencing templates
Use `TEMPLATE(name)` in any RESPOND value:
```yaml theme={null}
RESPOND: TEMPLATE(wire_confirmation)
```
#### Template interpolation
Template bodies use the same `{{variable_name}}` interpolation and `{{#if variable}}...{{/if}}` conditional-section syntax as any other ABL string — see [Template strings](/agent-platform/abl-reference/expressions-and-functions#template-strings) in Expressions and functions for the full syntax, including calling built-in functions inside `{{}}`.
```yaml theme={null}
DEFAULT: |
Hello, {{customer_name}}.
{{#if has_loyalty}}Your loyalty tier: {{loyalty_tier}}.{{/if}}
How can I help you today?
```
### Channel selection
The runtime selects the response format based on the delivery channel.
| Channel | Priority format | Fallback |
| - | - | - |
| Web/SDK | `RICH_CONTENT` (Markdown, HTML, AG-UI) | Plain text |
| Mobile | `RICH_CONTENT` (Adaptive Card, Carousel) | Plain text |
| Voice | `VOICE` (SSML, instructions, plain\_text) | RESPOND text |
| Slack | `RICH_CONTENT` (SLACK) | Markdown |
| WhatsApp | `RICH_CONTENT` (WHATSAPP) | Plain text |
If the preferred format isn't available, the runtime falls back through the priority chain until it finds a defined format. Plain text (`RESPOND`) is always the final fallback.
***
**Related articles:**
* [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) -- operators, built-in functions, `{{}}` template-string syntax, and type coercion rules
* [Lifecycle and hooks](/agent-platform/abl-reference/lifecycle-and-hooks) -- ON\_START and hooks that support rich content
* [Multi-Agent and supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) -- COMPLETE conditions with rich responses
* [Data Types and utilities](/agent-platform/abl-reference/data-types-and-utilities) -- types used in rich content and template fields
# ABL programming and scripting constructs
Source: https://koreai.mintlify.app/agent-platform/abl/reference/scripting-constructs
ABL supports two complementary execution styles:
* **Reasoning mode**: The agent has no `FLOW:` section, and the runtime decides how to pursue the goal.
* **Scripted flow mode**: The agent has a `FLOW:` section and follows authored steps, actions, guards, and transitions.
Use scripted flows when ordering, auditability, predictable cost, or strict business rules matter. Typical examples include payments, identity verification, regulated disclosures, onboarding forms, and support procedures with fixed escalation rules.
This guide treats ABL as a programming and logic language: variables, expressions, validation, branching, state reset, collection processing, tool calls, retries, and terminal paths. Agent coordination is an integration boundary rather than the focus of this page — see [Multi-Agent and Supervisor](/agent-platform/abl-reference/multi-agent-and-supervisor) for `DELEGATE`, `HANDOFF`, and `ESCALATE`. Every flow step must declare `REASONING: true` or `REASONING: false`.
***
## Minimal scripted flow example
```yaml theme={null}
AGENT: Account_Support
GOAL: "Help the customer resolve an account request."
FLOW:
start:
REASONING: false
RESPOND: "I can help with your account."
THEN: collect_account
collect_account:
REASONING: false
GATHER:
- account_id: required
THEN: lookup_account
lookup_account:
REASONING: false
CALL: get_account
WITH:
account_id: account_id
AS: accountResult
THEN: show_account
show_account:
REASONING: false
RESPOND: "Your account is {{accountResult.status}}."
THEN: COMPLETE
```
***
## Flow structure and execution
### FLOW
`FLOW:` defines the scripted state machine. Steps are named blocks. A step can transition to any other step, which allows linear flows, branches, retries, loops, and terminal paths.
Use it for:
* Fixed process ordering.
* Deterministic tool execution.
* Compliance or audit requirements.
* High-volume workflows where unnecessary LLM turns should be avoided.
```yaml theme={null}
FLOW:
start -> collect -> validate -> complete
```
The arrow notation communicates a sequence. The named step blocks contain the actual behavior.
Learn more: [Flow structure](/agent-platform/abl-reference/flow#flow-structure).
### REASONING
`REASONING` controls whether a step is deterministic or LLM-assisted:
```yaml theme={null}
FLOW:
deterministic_check:
REASONING: false
CHECK: amount > 0
THEN: process
interpret_request:
REASONING: true
GOAL: "Determine which supported request the customer is making."
EXIT_WHEN: request_type != null
THEN: route_request
```
Use `false` for explicit actions and boolean conditions. Use `true` when the step needs flexible interpretation, tool selection, or reasoning that can't be represented as deterministic expressions.
Reasoning-step controls include `GOAL`, `AVAILABLE_TOOLS`, `EXIT_WHEN`, `MAX_TURNS`, and `STEP_CONSTRAINTS`.
Learn more: [Per-step REASONING toggle](/agent-platform/abl-reference/flow#per-step-reasoning-toggle).
### WHEN
`WHEN` is an entry guard. The step runs only when its condition is true.
```yaml theme={null}
international_details:
REASONING: false
WHEN: transfer_type == "international"
GATHER:
- swift_code: required
THEN: validate_transfer
```
Learn more: [Entry guards](/agent-platform/abl-reference/flow#entry-guards).
### MAX\_ATTEMPTS and ON\_EXHAUSTED
`MAX_ATTEMPTS` limits retries for a step. `ON_EXHAUSTED` routes to a recovery step once the limit is reached.
```yaml theme={null}
verify_pin:
REASONING: false
MAX_ATTEMPTS: 3
ON_EXHAUSTED: lock_account
GATHER:
- pin: required
THEN: check_pin
```
Learn more: [Attempt limiting](/agent-platform/abl-reference/flow#attempt-limiting).
### THEN
`THEN` is the normal next transition.
```yaml theme={null}
complete_profile:
REASONING: false
SET: profile_complete = true
THEN: confirmation
```
`THEN` can point to a named step or a terminal action such as `COMPLETE`.
Learn more: [THEN / ON\_FAIL](/agent-platform/abl-reference/flow#then-%2F-on_fail).
***
## User interaction constructs
### RESPOND
`RESPOND` sends text to the user. Responses support `{{variable}}` interpolation and multiline pipe blocks. The FLOW reference sometimes labels this concept "SAY / RESPOND" — `RESPOND:` is the canonical flow action used in ABL examples.
```yaml theme={null}
summary:
REASONING: false
RESPOND: |
Please confirm:
Amount: {{amount}}
Destination: {{beneficiary_name}}
THEN: confirm
```
Use it for instructions, confirmations, validation messages, receipts, and error recovery.
Learn more: [SAY / RESPOND](/agent-platform/abl-reference/flow#say-%2F-respond).
### PRESENT
`PRESENT` shows context before a `GATHER` operation.
```yaml theme={null}
review:
REASONING: false
PRESENT: |
Name: {{beneficiary_name}}
Account: {{MASK(beneficiary_account, "last4")}}
Amount: {{FORMAT_CURRENCY(amount, "USD")}}
GATHER:
- confirmation: required
THEN: confirm
```
Learn more: [PRESENT](/agent-platform/abl-reference/flow#present).
### GATHER
`GATHER` collects one or more fields from the user. It can specify type, required status, prompt, defaults, validation, extraction strategy, and correction behavior.
```yaml theme={null}
collect_booking:
REASONING: false
GATHER:
FIELDS:
- destination: required
- checkin:
TYPE: date
REQUIRED: true
PROMPT: "When would you like to check in?"
- guests:
TYPE: number
DEFAULT: 2
STRATEGY: hybrid
CORRECTIONS: true
COMPLETE_WHEN: destination AND checkin
THEN: search
```
Use field validation for intrinsic values such as date format or account-number shape. Use `CHECK` or `CONSTRAINTS` for cross-field rules.
Learn more: [GATHER](/agent-platform/abl-reference/gather) for the full field-type and validation reference, or [GATHER in flow steps](/agent-platform/abl-reference/flow#gather-in-flow-steps) for flow-specific syntax.
***
## State and data constructs
### SET
`SET` assigns values to session variables. The right-hand side can be a literal, variable, dotted result path, or built-in expression.
```yaml theme={null}
initialize:
REASONING: false
SET:
status = "pending"
retry_count = 0
request_id = UNIQUE_ID(12)
created_at = NOW()
THEN: collect_details
```
Inline assignment is useful inside branches:
```yaml theme={null}
ON_INPUT:
- IF: input == "express"
SET: shipping_method = "express"
THEN: calculate_shipping
```
Use `SET` for normalization, derived values, flags, counters, tool-result extraction, and workflow IDs.
Learn more: [SET](/agent-platform/abl-reference/flow#set).
### CLEAR
`CLEAR` removes variables from session state. This is useful when the user changes an earlier choice and dependent values must be recollected.
```yaml theme={null}
change_destination:
REASONING: false
CLEAR: [destination, hotel_id, room_id, booking_total]
RESPOND: "Let's start with the new destination."
THEN: collect_destination
```
Clear all dependent values, not just the field the user changed.
Learn more: [CLEAR](/agent-platform/abl-reference/flow#clear).
### TRANSFORM
`TRANSFORM` applies a declarative array pipeline. The stages are `FILTER`, `MAP`, `SORT_BY`, and `LIMIT`.
```yaml theme={null}
prepare_results:
REASONING: false
TRANSFORM: searchResult.hotels AS hotel INTO visible_hotels
FILTER: hotel.rating >= 4
MAP:
name: hotel.name
price: FORMAT_CURRENCY(hotel.price_per_night, "USD")
rating: hotel.rating
SORT_BY: rating DESC
LIMIT: 5
THEN: show_results
```
Use `TRANSFORM` for search results, transaction lists, catalog filtering, reporting data, and safe presentation shaping. It avoids embedding collection-processing logic in prompts.
Learn more: [TRANSFORM](/agent-platform/abl-reference/flow#transform).
***
## Tool execution constructs
### CALL
`CALL` invokes a registered tool. Keep one tool call per step when sequencing matters, and chain calls through `THEN`.
```yaml theme={null}
CALL: get_balance
WITH:
account_id: account_id
AS: balanceResult
```
`WITH` maps tool parameters to literals, variables, dotted paths, or expressions. `AS` stores the result for later use.
Learn more: [Tools](/agent-platform/abl-reference/tools) for tool declaration and binding types.
### ON\_SUCCESS, ON\_FAILURE, and ON\_FAIL
`ON_SUCCESS` and `ON_FAILURE` handle the two basic outcomes of a tool call.
```yaml theme={null}
submit_request:
REASONING: false
CALL: create_ticket
WITH:
subject: subject
description: description
AS: ticketResult
ON_SUCCESS:
RESPOND: "Ticket {{ticketResult.id}} was created."
THEN: complete
ON_FAIL:
RESPOND: "I could not create the ticket."
THEN: retry_or_escalate
```
Use `ON_SUCCESS` and `ON_FAILURE` for structured success/failure handling around a `CALL`. Use `ON_RESULT` when the tool returns several business outcomes that require different paths. Use `ON_FAIL` for `CHECK` failure routing and other documented failure fallbacks. `ON_FAILURE` is also used by handoff configuration, so its meaning depends on where it appears.
Learn more: [ON\_SUCCESS / ON\_FAIL](/agent-platform/abl-reference/flow#on_success-%2F-on_fail).
### ON\_RESULT
`ON_RESULT` branches on fields in a tool result or deterministic flow context. Branches are evaluated in author order; the first matching branch wins.
```yaml theme={null}
validate_recipient:
REASONING: false
CALL: verify_recipient
WITH:
routing_number: routing_number
account_number: account_number
AS: recipientResult
ON_RESULT:
- IF: recipientResult.status == "valid"
SET:
recipient_name = recipientResult.account_holder
THEN: collect_amount
- IF: recipientResult.status == "account_closed"
RESPOND: "That account is closed. Please provide another account."
THEN: collect_recipient
- ELSE:
RESPOND: "The recipient details could not be verified."
THEN: collect_recipient
```
Learn more: [ON\_RESULT](/agent-platform/abl-reference/flow#on_result).
### CHECK
`CHECK` evaluates a deterministic boolean guard after state has been gathered or a tool has returned.
```yaml theme={null}
check_limits:
REASONING: false
CHECK: amount <= balanceResult.available AND amount <= daily_limit
ON_FAIL: over_limit
THEN: confirm_transfer
```
Use `CHECK` for local step prerequisites. If `CHECK` is false and no `ON_FAIL` target is provided, the step halts rather than automatically choosing a recovery path. Use `CONSTRAINTS` for reusable business rules that apply across multiple steps or boundaries.
Learn more: [CHECK](/agent-platform/abl-reference/flow#check).
### CONSTRAINTS
`CONSTRAINTS` declares reusable business invariants outside an individual step. Constraints are flattened and evaluated in declaration order on each turn; labels organize them but don't control execution order. A constraint can `REQUIRE`, `WARN`, `LIMIT`, or `RESTRICT` a condition, optionally scoped with `WHEN`, and define `ON_FAIL` behavior.
```yaml theme={null}
CONSTRAINTS:
transfer_rules:
- REQUIRE account_id != null
ON_FAIL: "I need to verify your account before continuing."
- LIMIT amount <= daily_limit
WHEN: amount != null
ON_FAIL: ESCALATE
```
Use constraints for rules that must remain true across several paths or at a tool/result boundary. Use `CHECK` when the rule is local to one flow step. `LIMIT` and `RESTRICT` remain distinct constraint kinds, but initially share the runtime handling path. If a failed constraint must clear state before retrying, add a `CLEAR:` list to the `ON_FAIL` block (or route to a dedicated step that performs `CLEAR`) — a plain string or `ESCALATE` `ON_FAIL` does not clear variables on its own. See [Memory and Constraints](/agent-platform/abl-reference/memory-and-constraints#constraints) for the full constraint syntax, severity levels, and evaluation order.
***
## Branching and navigation
### ON\_INPUT
`ON_INPUT` routes based on the current user input using deterministic boolean expressions.
```yaml theme={null}
confirm:
REASONING: false
RESPOND: "Would you like me to submit this request?"
ON_INPUT:
- IF: input == "yes" OR input == "confirm"
THEN: submit
- IF: input == "no" OR input == "cancel"
THEN: cancelled
- ELSE:
RESPOND: "Please answer yes or no."
THEN: confirm
```
Use `ON_INPUT` for explicit, reproducible choices. Don't use it to make an LLM semantic judgment — use `DIGRESSIONS` or a reasoning step for that.
Learn more: [ON\_INPUT](/agent-platform/abl-reference/flow#on_input).
### IF and ELSE
`IF` defines a branch condition. An omitted condition represents `ELSE`. Both appear in `ON_INPUT` and `ON_RESULT` branches.
### GOTO
`GOTO` jumps directly to a named step. It's useful for cancellation, correction, and recovery paths.
```yaml theme={null}
cancelled:
REASONING: false
CLEAR: [draft_request, confirmation]
RESPOND: "Your request has been cancelled."
THEN: COMPLETE
```
### DIGRESSIONS
`DIGRESSIONS` handle intent-based escapes from the current step. Unlike `ON_INPUT`, digressions are for semantic intents such as help, cancellation, or an urgent request.
```yaml theme={null}
collect_details:
REASONING: true
GATHER:
- account_id: required
DIGRESSIONS:
- INTENT: "help"
DO:
- RESPOND: "I need your account number to continue."
- RESUME
- INTENT: "cancel"
DO:
- RESPOND: "I cancelled the request."
- GOTO: cancelled
```
`global_digressions` can be declared under `FLOW` for behavior available in every step. A digression can `RESPOND`, `SET`, `CLEAR`, `CALL`, `DELEGATE`, `HANDOFF`, `GOTO`, or `RESUME`.
Learn more: [Digressions](/agent-platform/abl-reference/flow#digressions) and [Global digressions](/agent-platform/abl-reference/flow#global-digressions).
### SUB\_INTENTS
`SUB_INTENTS` handle step-local intents without leaving the step.
```yaml theme={null}
collect_address:
REASONING: false
GATHER:
- address: required
SUB_INTENTS:
- INTENT: "change address"
CLEAR: [address, delivery_quote]
RESPOND: "Please provide the new address."
- INTENT: "why do you need this"
RESPOND: "We use it to calculate delivery eligibility."
```
Learn more: [Sub-intents](/agent-platform/abl-reference/flow#sub-intents).
### COMPLETE\_WHEN
`COMPLETE_WHEN` defines when a collection step is complete. This is especially useful when several fields may be extracted from one user message.
```yaml theme={null}
collect_profile:
REASONING: false
GATHER:
FIELDS:
- name: required
- email: required
- phone: optional
COMPLETE_WHEN: name AND email
THEN: validate_profile
```
`COMPLETE_WHEN` controls collection completion. It isn't a replacement for a final business-rule `CHECK` or `CONSTRAINT`.
Learn more: [COMPLETE\_WHEN](/agent-platform/abl-reference/flow#complete_when).
***
## Programming and logic patterns
### Re-entry as looping
ABL doesn't use a general-purpose `WHILE` or `FOR` statement in an authored flow. A loop is expressed by transitioning back to a named step with `THEN` or `GOTO`. State variables provide the loop condition; `MAX_ATTEMPTS` bounds execution of a step, but it isn't a general-purpose loop predicate.
```yaml theme={null}
collect_code:
REASONING: false
MAX_ATTEMPTS: 3
ON_EXHAUSTED: lock_account
GATHER:
- verification_code: required
THEN: validate_code
validate_code:
REASONING: false
CALL: verify_code
WITH:
code: verification_code
AS: codeResult
ON_RESULT:
- IF: codeResult.valid == true
THEN: continue_flow
- ELSE:
RESPOND: "That code is not valid. Please try again."
THEN: collect_code
```
### Guarded state transitions
Use `WHEN`, `CHECK`, `ON_INPUT`, and `ON_RESULT` as the equivalent of guarded statements. Keep predicates side-effect-free and put mutations in `SET` or `CLEAR` actions.
```yaml theme={null}
approve:
REASONING: false
WHEN: request_status == "ready"
CHECK: reviewer_id != null AND amount <= approval_limit
ON_FAIL: approval_required
THEN: submit
```
### Counters and bounded retries
Use `SET` to maintain counters and `MAX_ATTEMPTS` to enforce a hard bound.
```yaml theme={null}
retryable_call:
REASONING: false
SET: attempt_count = ADD(COALESCE(attempt_count, 0), 1)
CALL: submit_request
AS: result
ON_FAILURE:
THEN: retry_or_escalate
THEN: complete
retry_or_escalate:
REASONING: false
CHECK: attempt_count < 3
ON_FAIL: escalate
THEN: retryable_call
```
### Terminal paths
`COMPLETE` ends the flow. `ESCALATE` ends the scripted path by routing to a human. Both should be explicit for success, cancellation, validation failure, and unrecoverable error paths.
```yaml theme={null}
complete:
REASONING: false
RESPOND: "Your request is complete."
THEN: COMPLETE
```
Learn more: [COMPLETE](/agent-platform/abl-reference/multi-agent-and-supervisor#complete) and [ESCALATE](/agent-platform/abl-reference/multi-agent-and-supervisor#escalate).
### ON\_ERROR
`ON_ERROR` handles named runtime or tool errors at the agent level or, with a `TYPE` entry, at an individual flow step. Use it for centralized recovery, retry, response, or escalation policy. Use `ON_FAILURE` for structured call failure handling and `ON_FAIL` for guard/fallback failure routing.
```yaml theme={null}
ON_ERROR:
tool_error:
RESPOND: "The service is taking too long. I will try once more."
SUBTYPE: tool_timeout
RETRY: 1
THEN: RETRY_STEP
validation_error:
RESPOND: "The service could not complete the request."
RETRY: 0
THEN: ESCALATE with REASON: "Tool failure after scripted recovery"
```
For escalation from an error handler, use `THEN: ESCALATE with REASON: "..."`. Don't use an unsupported nested escalation-priority form inside `ON_ERROR`. `THEN` in an `ON_ERROR` handler takes only one of a fixed set of actions (`CONTINUE`, `COMPLETE`, `ESCALATE`, `RETRY_STEP`, `HANDOFF`, `BACKTRACK`) — not a step name. The handler's own key is likewise matched against a fixed canonical set of error types (`default`, `llm_error`, `tool_error`, `routing_error`, `validation_error`, `timeout_error`, `unknown_error`); older names such as `tool_timeout`, `invalid_input`, and `api_error` are not valid types and never match — see [Error types](/agent-platform/abl-reference/lifecycle-and-hooks#error-types).
Step-level handlers can additionally narrow an error with `SUBTYPE`, add retry delay/backoff, or route validation failures back to a collection step:
```yaml theme={null}
search:
REASONING: false
CALL: search_catalog
ON_ERROR:
- TYPE: tool_error
SUBTYPE: timeout
RETRY: 2
RETRY_DELAY: 3000
RETRY_BACKOFF: exponential
THEN: ESCALATE with REASON: "Catalog search timed out"
```
The runtime's flow order is significant: response, gather, call, check, result/failure handling, input handling, then the default transition. Don't assume a later action runs after a branch has already transitioned.
Top-level `COMPLETE` rules can also define conditional completion responses. Interactive `ACTIONS` and `ON_ACTION` can be used as a typed input alternative to free-form `ON_INPUT` branching when the valid choices are known.
Learn more: [ON\_ERROR handlers](/agent-platform/abl-reference/lifecycle-and-hooks#on_error-handlers) for agent-level errors, or [step-level error handling](/agent-platform/abl-reference/flow#step-level-error-handling) for the `TYPE`/`SUBTYPE` form shown above.
***
## Built-in expression functions
Expressions can be used in `SET`, `CHECK`, `WHEN`, `ON_INPUT`, `ON_RESULT`, `FILTER`, `MAP`, and templates. Boolean expressions support comparisons and `AND`/`OR`/`NOT`. Keep them side-effect-free.
| Category | Functions |
| - | - |
| Math | `ADD`, `SUB`, `MUL`, `DIV`, `ROUND`, `ABS`, `MIN`, `MAX` |
| String | `UPPER`, `LOWER`, `TRIM`, `SUBSTRING`, `REPLACE`, `SPLIT`, `JOIN`, `PAD_START`, `PAD_END`, `REPEAT` |
| Formatting | `MASK`, `FORMAT_CURRENCY`, `FORMAT_DATE`, `ORDINAL` |
| Type | `IS_ARRAY`, `IS_NUMBER`, `IS_STRING`, `TO_NUMBER`, `TO_STRING` |
| Array | `LENGTH`, `ARRAY_FIND`, `ARRAY_FIND_INDEX` |
| Object | `OBJECT_KEYS`, `OBJECT_VALUES`, `OBJECT_MERGE` |
| Utility | `COALESCE`, `NOW`, `UNIQUE_ID` |
Example normalization:
```yaml theme={null}
SET:
normalized_phone = REPLACE(TRIM(phone), "-", "")
display_total = FORMAT_CURRENCY(total, "USD")
safe_account = MASK(account_number, "last4")
```
Guard nullable results before arithmetic or field access. `TO_NUMBER` can return `null`, and `DIV` returns `null` on division by zero — use `COALESCE`, `CHECK`, or a result branch before continuing. See [Expressions and functions](/agent-platform/abl-reference/expressions-and-functions) for the complete function reference, operator precedence, and type coercion rules.
***
## End-to-end example: funds transfer
The following example combines collection, normalization, tool calls, checks, result branching, correction, cancellation, and completion.
```yaml expandable=true theme={null}
AGENT: Funds_Transfer
GOAL: "Safely collect, validate, and submit a customer funds transfer."
TOOLS:
verify_recipient(routing_number: string, account_number: string)
-> {status: string, account_holder: string}
get_balance(account_id: string) -> {available: number}
create_transfer(account_id: string, recipient_account: string, amount: number)
-> {status: string, transfer_id: string, error: string}
FLOW:
global_digressions:
- INTENT: "cancel"
DO:
- RESPOND: "The transfer has been cancelled."
- CLEAR: [amount, recipient_account, transferResult]
- GOTO: cancelled
start:
REASONING: false
SET:
status = "collecting"
request_id = UNIQUE_ID(12)
THEN: collect_recipient
collect_recipient:
REASONING: false
GATHER:
FIELDS:
- routing_number: required
- account_number: required
CORRECTIONS: true
THEN: verify_recipient
verify_recipient:
REASONING: false
CALL: verify_recipient
WITH:
routing_number: routing_number
account_number: account_number
AS: recipientResult
ON_RESULT:
- IF: recipientResult.status == "valid"
SET:
recipient_name = recipientResult.account_holder
recipient_account = account_number
THEN: collect_amount
- IF: recipientResult.status == "account_closed"
RESPOND: "That account is closed. Please provide another account."
CLEAR: [account_number, recipient_account]
THEN: collect_recipient
- ELSE:
RESPOND: "I could not verify those details. Please check them."
THEN: collect_recipient
collect_amount:
REASONING: false
PRESENT: "Transferring to {{recipient_name}}."
GATHER:
- raw_amount:
TYPE: string
REQUIRED: true
SET:
amount = TO_NUMBER(REPLACE(TRIM(raw_amount), "$", ""))
CHECK: amount > 0
ON_FAIL: invalid_amount
THEN: load_balance
load_balance:
REASONING: false
CALL: get_balance
WITH:
account_id: account_id
AS: balanceResult
CHECK: amount <= balanceResult.available
ON_FAIL: insufficient_funds
THEN: confirm
confirm:
REASONING: false
RESPOND: |
Transfer {{FORMAT_CURRENCY(amount, "USD")}} to {{recipient_name}}?
Reply yes to submit or no to cancel.
ON_INPUT:
- IF: input == "yes" OR input == "confirm"
THEN: submit
- IF: input == "no" OR input == "cancel"
THEN: cancelled
- IF: input == "change"
CLEAR: [amount, raw_amount]
THEN: collect_amount
- ELSE:
RESPOND: "Please reply yes, no, or change."
THEN: confirm
submit:
REASONING: false
CALL: create_transfer
WITH:
account_id: account_id
recipient_account: recipient_account
amount: amount
AS: transferResult
ON_RESULT:
- IF: transferResult.status == "submitted"
RESPOND: "Transfer submitted: {{transferResult.transfer_id}}."
THEN: complete
- IF: transferResult.status == "duplicate"
RESPOND: "This transfer appears to have already been submitted."
THEN: complete
- ELSE:
RESPOND: "The transfer could not be submitted."
THEN: escalate
invalid_amount:
REASONING: false
RESPOND: "Please enter a positive transfer amount."
THEN: collect_amount
insufficient_funds:
REASONING: false
RESPOND: "The requested amount exceeds your available balance."
THEN: collect_amount
cancelled:
REASONING: false
RESPOND: "The transfer was cancelled."
THEN: COMPLETE
escalate:
REASONING: false
RESPOND: "I am connecting you with a specialist."
THEN: ESCALATE with REASON: "Transfer submission failed"
complete:
REASONING: false
SET:
status = "complete"
completed_at = NOW()
RESPOND: "Your transfer request is complete."
THEN: COMPLETE
```
***
## Choose the right construct
| Programming need | Preferred construct |
| - | - |
| Store or derive a value | `SET` |
| Reset stale or dependent state | `CLEAR` |
| Collect user data | `GATHER` |
| Show a review before collecting confirmation | `PRESENT` |
| Invoke a tool | `CALL` |
| Validate a local prerequisite | `CHECK` |
| Branch on explicit user input | `ON_INPUT` with `IF` / `ELSE` |
| Branch on tool business outcomes | `ON_RESULT` |
| Handle tool success versus failure | `ON_SUCCESS` / `ON_FAILURE` |
| Route a guard or documented fallback failure | `ON_FAIL` |
| Filter or shape a list | `TRANSFORM` |
| Jump to a recovery or cancellation step | `GOTO` or `THEN` |
| Handle semantic interruptions | `DIGRESSIONS` |
| Handle a step-local request | `SUB_INTENTS` |
| Bound a retry loop | `SET`, `CHECK`, `MAX_ATTEMPTS`, and re-entry via `THEN` |
| End processing | `COMPLETE` |
| Involve a human after scripted recovery is exhausted | `ESCALATE` |
| Offer known choices as typed input | `ACTIONS` and `ON_ACTION` |
***
## Design guidelines
1. Keep deterministic work in `REASONING: false` steps and reserve reasoning steps for interpretation or open-ended decisions.
2. Use one `CALL` per step when tool ordering and failure routing matter.
3. Prefer `ON_RESULT` for business statuses such as `valid`, `duplicate`, or `account_closed`. Use `ON_FAILURE` for tool execution failures and `ON_FAIL` for guard/fallback failures where documented.
4. Clear dependent state whenever an earlier user choice changes.
5. Use `ON_INPUT` for explicit boolean conditions and `DIGRESSIONS` for semantic intent handling.
6. Use `CHECK` for local prerequisites and `CONSTRAINTS` for reusable cross-step or boundary rules.
7. Shape arrays with `TRANSFORM` before presenting them to the user.
8. Make every normal, failure, cancellation, retry, and escalation path explicit.
9. Treat re-entry to a named step as a loop and always give it a bounded exit condition.
10. Keep calculations and data shaping in expressions and `TRANSFORM`. Keep user-facing wording in `RESPOND`.
11. Use `DELEGATE` and `HANDOFF` only when the problem genuinely crosses an agent ownership boundary.
# Tools
Source: https://koreai.mintlify.app/agent-platform/abl/reference/tools
The `TOOLS:` section defines the external capabilities available to an agent. Each tool declares a typed signature (parameters and return type), an optional execution binding, and metadata that guides the LLM's tool selection.
## Tool binding types
The `type:` property selects how a tool is executed. The supported values are:
| Type | Purpose |
| :- | :- |
| `http` | Call a REST (or SOAP) API endpoint. See [HTTP tools](#http-tools). |
| `mcp` | Bind a tool exposed by an MCP server. See [MCP tools](#mcp-tools). |
| `sandbox` | Run inline JavaScript/Python in an isolated sandbox. See [Code tools](#code-tools-sandbox). |
| `lambda` | Invoke a cloud serverless function. See [Lambda tools](#lambda-tools). |
| `async_webhook` | Fire-and-callback async HTTP. See [Async webhook tools](#async-webhook-tools). |
| `workflow` | Invoke a durable workflow on the workflow engine. See [Workflow tools](#workflow-tools). |
| `searchai` | Query a SearchAI knowledge base. See [SearchAI tools](#searchai-tools). |
| `table` | Structured CRUD against an Agent Table. See [Table tools](#table-tools). |
## Tool declaration syntax
You declare a tool with a function-style signature followed by indented properties.
```yaml theme={null}
TOOLS:
tool_name(param1: type1, param2: type2 = default) -> ReturnType
description: "What this tool does"
type: http
endpoint: "/api/path"
method: POST
```
### Signature format
```yaml theme={null}
name(parameters) -> return_type
```
* **name** -- a lowercase identifier using `snake_case`
* **parameters** -- comma-separated list of typed parameters
* **return\_type** -- the type of data the tool returns
### Parameter syntax
Each parameter follows the format `name: type`, optionally marked optional with `?` and/or given a default value:
```yaml theme={null}
param_name: type
param_name?: type # optional (no default)
param_name: type = default_value
```
| Component | Description | Examples |
| - | - | - |
| `name` | Parameter identifier (snake\_case) | `account_id`, `query`, `limit` |
| `?` | Optional marker after the name — makes the parameter optional without giving it a default | `note?: string` |
| `type` | Data type | `string`, `number`, `boolean`, `date` |
| `= default` | Default value (also makes the parameter optional) | `= 10`, `= "USD"`, `= true` |
A parameter is required only when it has **neither** a `?` marker **nor** a default value. Both `name?: type` and `name: type = default` make it optional.
### Parameter types
| Type | Description | Examples |
| - | - | - |
| `string` | Text value | `"hello"` |
| `number` | Numeric value (integer or float) | `42`, `3.14` |
| `boolean` | True or false | `true`, `false` |
| `date` | Date value | `"2026-03-01"` |
| `object` | Nested object (specify fields with nested `parameters:`) | -- |
| `type[]` | Array of the given type | `string[]`, `number[]` |
#### Object parameters
When a parameter has type `object`, you can define nested fields using a `parameters:` block under the tool:
```yaml theme={null}
TOOLS:
create_order(customer: object, items: object[]) -> {order_id: string}
description: "Create a new order"
parameters:
customer:
name: string
email: string
items:
product_id: string
quantity: number
```
#### Array parameters
Array types use the `[]` suffix:
```yaml theme={null}
TOOLS:
bulk_lookup(ids: string[]) -> {results: object[]}
description: "Look up multiple records by ID"
```
#### Parameter enrichment and sources
The `parameters:` block does more than describe nested object fields — it can enrich any signature-declared parameter with a description, validation, visibility, and a **value source**. This lets you inject values (from system state, config, auth, an expression, or a lookup table) without the LLM having to supply them, and optionally hide them from the model.
```yaml theme={null}
TOOLS:
charge_card(amount: number, customer_id: string) -> {receipt_id: string}
description: "Charge the customer's card on file"
type: http
endpoint: "/api/charges"
method: POST
parameters:
customer_id:
source: system # model | system | runtime | config | auth | expr | lookup
value: "session.customer_id"
hidden: true # do not expose to the model
amount:
description: "Charge amount in the account currency"
required: true
```
| Enrichment key | Type | Description |
| - | - | - |
| `description` | `string` | Parameter description shown to the model. |
| `required` | `boolean` | Whether the parameter is required. |
| `default` | any | Default value. |
| `source` | `string` | Value source: `model`, `system`, `runtime`, `config`, `auth`, `expr`, `lookup`. |
| `value` | `string` | The expression or reference used with `expr`/`lookup`/`system`/`config` sources. |
| `key` / `table` | `string` | Lookup key and table (with `source: lookup`). |
| `hidden` | `boolean` | Hide the parameter from the model (still sent to the tool). |
| `visibility` | `object` | Fine-grained `{ model, trace }` visibility flags. |
| `properties` | `object` | Nested object field schema (for `object` params). |
| `items` | `object` | Item schema (for array params). |
`default_source` is accepted as an alias for `source` (both set the parameter's value source). Some compile-time diagnostics — for example the [trusted SIP transfer destination](#trusted-sip-transfer-destinations) check — refer to the key by its `default_source` spelling.
#### Trusted SIP transfer destinations
The built-in `call_transfer` voice tool takes a `sipTransferId` parameter — a `sip:`/`sips:` endpoint URI. To pin a **fixed, trusted** transfer target for the agent (rather than letting the model choose one, or relying on project settings), declare `call_transfer` with its signature in `TOOLS:` and enrich the `sipTransferId` parameter with a literal `default` and `default_source: config`:
```yaml theme={null}
TOOLS:
# Pin the agent's SIP transfer destination on the built-in call_transfer tool.
call_transfer(callTransferType: string, sipTransferId: string) -> object
params: # `parameters:` is an accepted alias
sipTransferId:
default: "sip:premium-support@sbc.example.com"
default_source: config
```
The `params:` block enriches the signature-parsed parameter by name — it does not redeclare the built-in's implementation. Marking the default `default_source: config` does two things: the runtime reads the destination **server-side from the compiled agent** (never from the tool-call arguments, which carry model- and caller-influenced values), and it **hides the parameter from the model's tool schema** entirely, so the LLM never sees a destination field to fill.
At SIP transfer time the runtime resolves the destination in order: (1) the authored `sipTransferId` default, if present and valid; (2) the project-configured `voice.sipTransferTargetUri` (Agent Transfer settings); (3) otherwise the call fails with a missing-target error. `default_source: system` is treated as trusted the same way (for platform-injected declarations).
The compiler enforces that an authored destination is safe to trust:
| Diagnostic | Severity | Fires when |
| - | - | - |
| `INVALID_SIP_TRANSFER_DESTINATION` | Error | A trusted-source (`config`/`system`) `sipTransferId` default is empty, is not a valid `sip:`/`sips:` URI, uses a dynamic source (`expr`, `lookup`, `runtime`, `auth`), or contains a `{{ }}` template other than an admitted inbound-header placeholder (see [Inbound SIP header placeholders](#inbound-sip-header-placeholders)). The destination's user/host must be a static literal — a target derived from conversation data could be influenced by the caller. |
| `UNTRUSTED_SIP_TRANSFER_DESTINATION` | Warning | A `sipTransferId` default is declared with `default_source` **absent or `model`**. The value is treated as model-supplied and is **ignored at transfer time** (behavior is unchanged from before) — add `default_source: config` to make it the agent's trusted destination. |
Omit the `sipTransferId` default entirely to rely solely on the project-configured Agent Transfer target.
##### Inbound SIP header placeholders
The one template form allowed in a `sipTransferId` default is `{{sip.}}`. At transfer time the runtime replaces it with the value of an inbound SIP header from the **current call's trusted SIP metadata** (the headers the call arrived with) — never from conversation data or model output. Use it to forward an inbound header onto the transfer leg, for example a `User-to-User` correlation token or a tracing header:
```yaml theme={null}
TOOLS:
call_transfer(callTransferType: string, sipTransferId: string) -> object
params:
sipTransferId:
default: "sip:premium-support@sbc.example.com?User-to-User={{sip.user-to-user}}&X-Trace-Id={{sip.x-trace-id}}"
default_source: config
```
**Syntax.** `{{sip.}}`, where `` is the inbound SIP header name, matched case-insensitively (for example `{{sip.user-to-user}}` reads the inbound `User-to-User` header).
Placeholders are constrained so inbound data can *decorate* the transfer but never *redirect* it. The compiler rejects the default with `INVALID_SIP_TRANSFER_DESTINATION` unless every placeholder satisfies all of:
| Rule | Requirement |
| - | - |
| **Position** | A placeholder may appear only in the URI's header section (the `?Name=value&…` part after `?`). A placeholder anywhere in the user/host — the part that determines where the call is dialed — is rejected (`placeholder_outside_headers`). |
| **Whole value only** | A placeholder must be the *entire* value of a URI header (`?X-Trace-Id={{sip.x-trace-id}}`), not embedded in a larger value and not partial. |
| **Value, not name** | A placeholder may never appear in a URI-header *name* (`placeholder_in_header_name`) — an inbound value must not become a header key. |
| **Admitted destination header** | Only `User-to-User` and any `x-*` header may *carry* a placeholder value (`inadmissible_header_name` otherwise). |
| **Admitted source header** | The inbound header *read from* — the `` in `{{sip.}}` — must **also** be admissible (`User-to-User` or `x-*`), so an admitted destination header cannot launder an arbitrary inbound header's value (`inadmissible_source_header` otherwise). |
Any other use of `{{ }}` — a general expression/variable template, a placeholder in the user/host, or a malformed placeholder — remains an error; only `{{sip.}}` in an admitted header value is interpolated. The URI's user and host are always static.
##### Dynamic destinations
For a destination chosen at runtime, replace the static `default` with a `template` and an `approved_authorities` allow-list. The template is a **two-zone** SIP URI: a static shape whose authority (user/host/port) is filled from `{{route.*}}` placeholders, plus the same inbound `{{sip.}}` header decoration described above. The `{{route.*}}` values are produced by a [`RESOLVE_SIP_DESTINATION`](/agent-platform/abl/reference/flow#resolve_sip_destination) flow step, which runs before the transfer and commits the chosen authority to a trusted server-side routing channel.
```yaml theme={null}
TOOLS:
call_transfer(callTransferType: string, sipTransferId: string) -> object
params:
sipTransferId:
default_source: config
template: "sip:{{route.vdn}}@{{route.ip}}:{{route.port}};user=phone?User-to-User={{sip.user-to-user}}"
approved_authorities:
- "sip:521001@172.0.41.7:5060"
- "sip:521002@172.0.41.7:5060"
- "sip:5220[0-9][0-9]@172.0.41.7:5060"
```
* **Zone 1 — authority** (before `?`): `{{route.vdn}}`, `{{route.ip}}`, `{{route.port}}` are filled from the `RESOLVE_SIP_DESTINATION` feeder's result — its return shape must use exactly those keys.
* **Zone 2 — headers** (after `?`): `{{sip.}}` placeholders, resolved server-side from trusted inbound SIP metadata, under the same rules as static defaults above.
* **`approved_authorities`** is a mandatory allow-list — the resolved destination must match one entry or the transfer is refused. Each entry must be a **literal** SIP authority with an explicit scheme, host, and port (no default-port inference, no wildcard host). The VDN (user part) may be an exact digit string or a constrained digit-class pattern using single-digit ranges only — for example, `5220[0-9][0-9]`. Regex metacharacters, alternation, anchors, groups, and any query/header/param section are rejected.
The compiler validates both surfaces against the same predicates the runtime matcher uses (so a compile-accepted entry is never runtime-rejected): a malformed template raises `INVALID_SIP_TRANSFER_DESTINATION`; a bad allow-list entry raises `INVALID_APPROVED_SIP_AUTHORITY` (not a literal authority with a literal VDN, host, and explicit port) or `UNSAFE_SIP_AUTHORITY_PATTERN` (unsafe VDN pattern).
### Return type
The return type appears after the `->` arrow. It can be:
| Form | Syntax | Example |
| - | - | - |
| Simple type | `-> type` | `-> string` |
| Object type | `-> {field: type, ...}` | `-> {id: string, name: string}` |
| Array type | `-> type[]` | `-> Hotel[]` |
| Nested object | `-> {field: {nested: type}}` | `-> {user: {name: string, age: number}}` |
| Optional fields | `-> {field?: type}` | `-> {error?: string}` |
| Array of objects | `-> {field: type}[]` | `-> {id: string, title: string}[]` |
```yaml theme={null}
TOOLS:
search_hotels(destination: string, checkin: date, checkout: date) -> {hotels: {id: string, name: string, price: number, rating: number}[], total: number}
description: "Search available hotels"
```
### Description
The `description:` property provides a natural-language explanation of what the tool does. The LLM uses this description to decide when and how to call the tool.
```yaml theme={null}
TOOLS:
verify_account(account_id: string) -> {name: string, status: string}
description: "Retrieve account details and verify the account is active"
```
Write meaningful descriptions that help the LLM understand:
* What the tool does
* When to call it
* What data it returns
### Hints
The `hints:` block provides execution metadata that the runtime uses for optimization:
```yaml theme={null}
TOOLS:
get_balance(account_id: string) -> {balance: number}
description: "Get account balance"
hints:
cacheable: true
latency: fast
timeout: 5000
```
| Hint | Type | Default | Description |
| - | - | - | - |
| `cacheable` | `boolean` | *none* | Whether results can be cached across calls with the same parameters |
| `latency` | `"fast"` , `"medium"` , `"slow"` | *none* | Expected latency category for runtime scheduling |
| `side_effects` | `boolean` | *none* | Whether the tool modifies external state |
| `requires_auth` | `boolean` | *none* | Whether the tool requires authenticated context |
| `timeout` | `number` | *none* | Tool-specific timeout in milliseconds (overrides `EXECUTION.tool_timeout`) |
***
## HTTP tools
HTTP tools call REST API endpoints. They're the most common tool type.
```yaml theme={null}
TOOLS:
search_flights(origin: string, destination: string, date: date) -> {flights: {id: string, price: number}[]}
description: "Search available flights"
type: http
endpoint: "https://api.flights.com/v1/search"
method: POST
auth: bearer
timeout: 10000
retry: 3
retry_delay: 1000
```
### HTTP binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"http"` | Yes | -- | Declares this as an HTTP tool |
| `endpoint` | `string` | Yes | -- | URL or path for the API call. Supports path parameters: `"/hotels/{hotel_id}"` |
| `method` | `"GET"` , `"HEAD"` , `"OPTIONS"` , `"POST"` , `"PUT"` , `"PATCH"` , `"DELETE"` | Yes | -- | HTTP method |
| `idempotency_header` | `string` | No | *none* | Header name the runtime sends a durable dedupe key under (for example `Idempotency-Key`). Must name a real HTTP header, be ≤128 characters, and cannot be a credential/cookie/transport/signing header (`Authorization`, `Cookie`, `Host`, `Content-Type`, `X-Amz-*`, `SOAPAction`, etc.) — an invalid value is a compile warning (`HTTP_TOOL_INVALID_IDEMPOTENCY_HEADER`) and the runtime sends no idempotency key. Omit to keep the key internal. |
| `auth` | `string` | No | `"none"` | Authentication type (see [Auth](#auth)) |
| `timeout` | `number` | No | `EXECUTION.tool_timeout` | Request timeout in milliseconds |
| `retry` | `number` | No | *none* | Number of retry attempts on failure |
| `retry_delay` | `number` | No | *none* | Delay in milliseconds between retries |
| `retry_policy` | `object` | No | *none* | Structured backoff policy (richer alternative to `retry`/`retry_delay`). See [Retry policy](#retry-policy). |
| `headers` | `Record` | No | *none* | Custom HTTP headers |
| `query_params` | `Record` / `object[]` | No | *none* | Query string parameters. A `key: value` block keeps insertion order; an array of `{key, value}` also allows duplicate keys. |
| `body_type` | `"json"` , `"form"` , `"xml"` , `"text"` | No | *none* | Request body encoding. |
| `body_mode` | `"auto"` , `"none"` | No | `"auto"` | For non-GET methods: `auto` serializes remaining params as the body; `none` sends no body. |
| `body_template` | `string` | No | *none* | Custom body template with interpolation. `body` is accepted as an alias. |
| `body_schema` | `string` | No | *none* | JSON Schema for the request body (used with `use_body_schema: true`). |
| `use_body_schema` | `boolean` | No | `false` | Author/validate the request body from `body_schema`. |
| `request_variants` | `object[]` | No | *none* | Conditional request variants selected by a `when` expression. See [Request variants](#request-variants). |
| `rate_limit` | `number` | No | *none* | Maximum requests per second |
| `circuit_breaker` | `{threshold, resetMs}` | No | *none* | Circuit breaker configuration |
| `protocol` | `"rest"` , `"soap"` | No | `"rest"` | Wire protocol. Set `soap` for SOAP endpoints. See [SOAP support](#soap-support). |
| `soap_version` | `"1.1"` , `"1.2"` | No | `"1.1"` | SOAP version (when `protocol: soap`). |
| `soap_action` | `string` | No | *none* | `SOAPAction` header (1.1) or action media-type parameter (1.2). |
| `on_soap_fault` | `"error"` , `"data"` | No | *none* | How to handle `` — `error` throws, `data` returns the fault payload. |
| `wsdl_*` | see [WSDL operation binding](#wsdl-operation-binding) | No | *none* | Bind a SOAP tool to a stored WSDL operation instead of authoring the envelope. |
### Path parameters
Use curly braces in the endpoint to reference tool parameters:
```yaml theme={null}
TOOLS:
get_hotel(hotel_id: string) -> Hotel
type: http
endpoint: "/hotels/{hotel_id}"
method: GET
```
### Headers
Custom headers are specified as key-value pairs:
```yaml theme={null}
TOOLS:
get_data(query: string) -> {results: object[]}
type: http
endpoint: "/api/data"
method: POST
headers:
X-Api-Version: "2024-01"
Accept: "application/json"
```
On a recognized auth header — `Authorization`, `Proxy-Authorization`, `X-Api-Key`, `Api-Key`, or `X-Auth-Token` (case-insensitive) — a literal value that looks like a real credential (a JWT, a known provider token prefix, a `Bearer `/`Basic `/`Token ` scheme, or another long opaque token) is a compile warning (`PLAINTEXT_HTTP_HEADER_SECRET`). The authored request still compiles and runs unchanged, but that value can cross export, logging, or runtime boundaries with no secret-management protection. Use `{{config.API_TOKEN}}` or an auth profile instead of a literal; a `{{ }}` reference is never flagged.
### Auth
The `auth:` property specifies the authentication mechanism. The runtime resolves credentials from the project's credential store -- the ABL document never contains actual secrets.
| Auth type | Description |
| - | - |
| `none` | No authentication |
| `bearer` | Bearer token in `Authorization` header |
| `api_key` | API key (header name configurable via `auth_config`) |
| `oauth2_client` | OAuth 2.0 client credentials flow |
| `oauth2_user` | OAuth 2.0 authorization code flow (user-scoped tokens) |
| `saml` | SAML assertion-based authentication |
| `custom` | Custom authentication with configurable headers |
`saml` is accepted by the parser but is **not currently resolved at runtime** — it is dropped during compilation. Prefer an [auth profile](#auth-profiles-jit-consent-and-connections) for enterprise SSO-backed integrations.
#### Auth configuration
For auth types that require additional configuration, use the `auth_config:` block:
```yaml theme={null}
TOOLS:
get_user_data(user_id: string) -> {name: string, email: string}
type: http
endpoint: "/api/users/{user_id}"
method: GET
auth: oauth2_client
auth_config:
token_url: "https://auth.example.com/oauth/token"
client_id: "my-client-id"
client_secret: "{{config.OAUTH_CLIENT_SECRET}}"
scopes: "read:users"
```
```yaml theme={null}
TOOLS:
call_partner_api(payload: string) -> {status: string}
type: http
endpoint: "https://partner.example.com/api"
method: POST
auth: api_key
auth_config:
header_name: "X-API-Key"
```
| Auth config property | Type | Used with | Description |
| - | - | - | - |
| `token_url` | `string` | `oauth2_client`, `oauth2_user` | OAuth token endpoint URL |
| `client_id` | `string` | `oauth2_client`, `oauth2_user` | OAuth client identifier |
| `client_secret` | `string` | `oauth2_client`, `oauth2_user` | OAuth client secret (use `{{config.X}}` placeholders) |
| `scopes` | `string` | `oauth2_client`, `oauth2_user` | Space-separated OAuth scopes |
| `header_name` | `string` | `api_key` | Custom header name for the API key |
| `provider` | `string` | `oauth2_user`, `saml` | Auth provider identifier |
| `custom_headers` | `Record` | `custom` | Custom authentication headers |
#### Credential placeholders
Credentials use `{{config.NAME}}` placeholder syntax (with `isSecret: true` on the config var). The runtime resolves these from the project's config vars at execution time. For OAuth integrations, binding an auth profile is the preferred approach. Never embed actual credentials in ABL files.
```yaml theme={null}
auth_config:
client_secret: "{{config.PARTNER_OAUTH_SECRET}}"
```
#### Auth profiles, JIT, consent, and connections
For OAuth and enterprise integrations, the preferred approach is to bind a pre-configured **auth profile** by name rather than declaring `auth_config` inline. These tool-level properties control credential resolution, transport security, just-in-time authorization, consent, and per-user vs. shared connections:
```yaml theme={null}
TOOLS:
create_ticket(subject: string, body: string) -> {ticket_id: string}
description: "Create a support ticket"
type: http
endpoint: "https://itsm.example.com/api/tickets"
method: POST
auth_profile: servicenow_oauth # named profile resolved at runtime
tls_profile: partner_mtls # mTLS transport cert profile
auth_jit: true # acquire credentials just-in-time
consent: preflight # preflight | inline
connection: per_user # per_user | shared
```
| Property | Type | Description |
| - | - | - |
| `auth_profile` | `string` | Name of a pre-configured auth profile; resolved at runtime (no secrets in the ABL file). |
| `tls_profile` | `string` | Name of an mTLS transport-certificate profile (independent of `auth_profile`). |
| `auth_jit` | `boolean` | Acquire credentials just-in-time at call time rather than up front. |
| `consent` | `"preflight"` , `"inline"` | When user consent is obtained — before the run (`preflight`) or during the conversation (`inline`). |
| `connection` | `"per_user"` , `"shared"` | Whether the integration uses per-end-user connections or a single shared connection. |
### Result transformation
Tool results are available in the session context after execution. You can map specific result fields to session variables using `on_result:` and handle errors with `on_error:`:
```yaml theme={null}
TOOLS:
get_balance(account_id: string) -> {balance: number, currency: string}
description: "Get account balance"
type: http
endpoint: "/api/balance"
method: GET
on_result:
set:
current_balance: result.balance
account_currency: result.currency
on_error:
set:
balance_error: error.message
```
Beyond `set:`, both `on_result:` and `on_error:` accept an ordered `do:` action block that can run `SET`, `CLEAR`, `LOG`, `RESPOND`, `CALL`, `GOTO`/`THEN`, `HANDOFF`, `DELEGATE`, `RETURN`, `FORMATS` (or its canonical alias `RICH_CONTENT`), and `COMPLETE` actions, with conditional `IF:`/`WHEN:`/`ELSE:` branches. A rich-content payload rendered this way — for example a `FEEDBACK` survey attached to a tool's result — works with either spelling.
A `do:` action's `RESPOND` can carry its own `MODEL_CONTEXT` — a text-scalar sibling, the same idea as [MODEL\_CONTEXT on a response](/agent-platform/abl-reference/flow#model_context-on-a-response), applied here to a tool lifecycle response instead of a FLOW step's:
```yaml theme={null}
TOOLS:
list_products() -> object
MODEL_CONTEXT:
FIELDS: [products[].id, products[].name, total]
on_result:
do:
- RESPOND: "{{result.widget}}"
MODEL_CONTEXT: "Displayed {{result.total}} products in order: {{result.product_summary}}."
COMPLETE:
on_error:
do:
- RESPOND: "Product search is unavailable."
MODEL_CONTEXT: "Product search failed; no products were displayed."
COMPLETE:
```
The full, protected `RESPOND` is still what's delivered and retained in the customer transcript — `MODEL_CONTEXT` only replaces what a *later* model call sees of this specific assistant turn. `result`/`error`, the tool's inputs, and any earlier `SET` in the same action are available to both templates. `MODEL_CONTEXT` without a `RESPOND` in the same action is invalid (`MODEL_CONTEXT_INVALID`). Omit it to keep the historical full-response behavior; an explicitly empty value omits that response from later model calls entirely. This is independent of the tool-level `MODEL_CONTEXT` block above — one shapes the raw tool result the model sees, the other shapes what the model retains of this particular lifecycle response.
### SSRF protection
The platform enforces SSRF (Server-Side Request Forgery) protection on all HTTP tool endpoints. Private IP ranges, localhost, and internal network addresses are blocked at the runtime level. Only allowlisted domains and public endpoints are permitted for HTTP tool calls.
### Circuit breaker
The circuit breaker prevents cascading failures by halting requests to a failing endpoint after a threshold of consecutive errors:
```yaml theme={null}
TOOLS:
unreliable_api(query: string) -> {data: string}
type: http
endpoint: "https://api.example.com/data"
method: GET
circuit_breaker:
threshold: 5
resetMs: 30000
```
| Property | Type | Description |
| - | - | - |
| `threshold` | `number` | Number of consecutive failures before the circuit opens |
| `resetMs` | `number` | Milliseconds to wait before trying the endpoint again |
### Retry policy
`retry_policy` is a structured backoff configuration — a richer alternative to the scalar `retry`/`retry_delay` fields (both forms are still accepted).
```yaml theme={null}
TOOLS:
flaky_api(id: string) -> {data: string}
type: http
endpoint: "https://api.example.com/{id}"
method: GET
retry_policy:
strategy: exponential # fixed | exponential
initial_delay_ms: 200
multiplier: 2
max_delay_ms: 5000
max_retries: 4
jitter: full # none | full
```
| Property | Type | Default | Description |
| - | - | - | - |
| `strategy` | `"fixed"` , `"exponential"` | -- | Backoff strategy (required). |
| `initial_delay_ms` | `number` | -- | Delay before the first retry. |
| `multiplier` | `number` | -- | Growth factor per attempt (for `exponential`). |
| `max_delay_ms` | `number` | -- | Ceiling on the delay between attempts. |
| `max_retries` | `number` | -- | Maximum retry attempts. |
| `jitter` | `"none"` , `"full"` | -- | Randomization applied to the delay. |
### SOAP support
HTTP tools can call SOAP endpoints by setting `protocol: soap`.
```yaml theme={null}
TOOLS:
lookup_policy(policy_number: string) -> {status: string, holder: string}
description: "Look up an insurance policy"
type: http
endpoint: "https://legacy.example.com/PolicyService"
method: POST
protocol: soap
soap_version: "1.1"
soap_action: "urn:PolicyService#lookupPolicy"
on_soap_fault: error # error | data
soap_headers: '{"Security": {"UsernameToken": {"Username": "svc"}}}'
```
| Property | Type | Default | Description |
| - | - | - | - |
| `protocol` | `"rest"` , `"soap"` | `"rest"` | Wire protocol. |
| `soap_version` | `"1.1"` , `"1.2"` | `"1.1"` | SOAP envelope version. |
| `soap_action` | `string` | *none* | `SOAPAction` header (1.1) or action media-type parameter (1.2). |
| `on_soap_fault` | `"error"` , `"data"` | *none* | `error` raises on ``; `data` returns the fault as the result. |
| `soap_headers` | JSON object (as a string) | *none* | Custom SOAP header elements, authored as a JSON string. Malformed JSON is dropped rather than failing the tool compile. |
#### WSDL operation binding
Instead of hand-writing the SOAP envelope, a SOAP tool can bind to an operation from a WSDL asset stored in the project [WSDL Repository](/agent-platform/tools/wsdl-repo). When bound, the runtime resolves the operation's namespace, `SOAPAction`, and SOAP version from the stored WSDL and **generates the request body from the tool's JSON input** — no manual `body` template is needed. The request `endpoint` still comes from the tool: bulk-created tools are populated with the WSDL service address (or an endpoint override), and hand-authored tools must set `endpoint` explicitly.
Reference the WSDL by name; the platform resolves it to a stable asset/version at save time:
```yaml theme={null}
TOOLS:
lookup_policy(policy_number: string) -> {status: string, holder: string}
description: "Look up an insurance policy"
type: http
endpoint: "https://legacy.example.com/PolicyService"
method: POST
protocol: soap
soap_version: "1.1"
wsdl_asset: "PolicyService" # asset name in the WSDL Repository
wsdl_version: latest # version number or "latest" (default)
wsdl_operation: "lookupPolicy"
```
| Property | Type | Required | Description |
| - | - | - | - |
| `wsdl_asset` | `string` | No | WSDL asset **name** to bind. Resolved to `wsdl_asset_id` at save time. |
| `wsdl_version` | `string` | No | Version number (for example `2`) or `latest`. Defaults to the asset's latest version. |
| `wsdl_operation` | `string` | Yes\* | Name of the WSDL operation to invoke. |
| `wsdl_asset_id` | `string` | Yes\* | Stable asset ID. Written automatically when `wsdl_asset` is resolved; may be authored directly. |
| `wsdl_version_id` | `string` | Yes\* | Stable immutable version ID. Written automatically when `wsdl_version` is resolved. |
| `wsdl_service` | `string` | No | WSDL `service` name (when an operation is ambiguous across services). |
| `wsdl_port` | `string` | No | WSDL `port` name. |
| `wsdl_binding` | `string` | No | WSDL `binding` name. |
| `wsdl_target_namespace` | `string` | No | Target namespace override for body generation. |
| `wsdl_element` | `string` | No | Root request element name, when it differs from the operation name. |
| `wsdl_response_arrays` | comma-separated `string` | No | Response paths (for example `Envelope.Body.Response.Items,Envelope.Body.Response.Errors`) that should be parsed as arrays. |
A WSDL binding requires `wsdl_operation` plus a resolved asset/version. Author either the by-name pair (`wsdl_asset` + optional `wsdl_version`) — which the platform rewrites to `wsdl_asset_id` / `wsdl_version_id` on save — or the stable IDs directly. Versions are immutable: a bound tool keeps calling its pinned version until you repoint it, so a WSDL update never silently changes behavior.
If a WSDL construct is unsupported, author a manual SOAP tool with an explicit `body` template instead — the binding fields and a manual body are mutually exclusive per tool.
### Request variants
For endpoints that vary by condition, `request_variants` selects a request shape at call time based on a `when` expression. Each variant can override the endpoint, method, headers, query params, and body.
```yaml theme={null}
TOOLS:
submit(order: object, region: string) -> {ref: string}
type: http
endpoint: "https://api.example.com/us/submit"
method: POST
request_variants:
- name: eu
when: region == "EU"
endpoint: "https://api.eu.example.com/submit"
body_type: json
- name: default
default: true
```
***
## MCP tools
MCP (Model Context Protocol) tools connect to external MCP servers that expose tools dynamically. The runtime handles protocol negotiation, transport, and tool discovery.
```yaml theme={null}
TOOLS:
web_search(query: string) -> {results: {title: string, url: string, snippet: string}[]}
description: "Search the web"
type: mcp
server: "brave-search"
```
### MCP binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"mcp"` | Yes | -- | Declares this as an MCP tool |
| `server` | `string` | Yes | -- | MCP server name (resolved from runtime configuration) |
| `server_tool` | `string` | No | Tool name | Tool name on the MCP server (if different from the ABL tool name) |
| `output_schema` | JSON object (string) | No | -- | JSON Schema describing the tool's result, as a single JSON object literal — for example `{"type": "object", "properties": {...}}`. Invalid JSON is dropped (`TOOL_OUTPUT_SCHEMA_INVALID`). |
| `headers` | `Record` | No | *none* | Per-call headers, supporting `{{config.X}}` / `{{session.X}}` templates. |
### Server configuration
The `server` value is a logical name that maps to an MCP server configuration in the project's runtime settings. Server configuration (transport type, connection URL, authentication) is managed at the project level, not in the ABL file.
The platform supports these MCP transport types.
| Transport | Description |
| - | - |
| `stdio` | Standard input/output (for local MCP server processes) |
| `http` | HTTP-based transport (Streamable HTTP) |
| `websocket` | WebSocket-based transport |
### Dynamic tool discovery
MCP servers can expose multiple tools. When you declare an MCP tool in ABL, you bind a specific tool from the server. If the tool name on the MCP server differs from the tool name in your ABL file, use `server_tool:` to specify the server-side name.
```yaml theme={null}
TOOLS:
search(query: string) -> {results: object[]}
description: "Search using Brave"
type: mcp
server: "brave-search"
server_tool: "brave_web_search"
```
***
## Code tools (sandbox)
Code tools execute user-provided JavaScript or Python code in an isolated sandbox environment with resource limits and no network access.
```yaml theme={null}
TOOLS:
calculate_tax(income: number, state: string) -> {tax: number, rate: number}
description: "Calculate state income tax"
type: sandbox
runtime: javascript
timeout: 5000
memory_mb: 128
code: |
const rates = { CA: 0.133, NY: 0.109, TX: 0 };
const rate = rates[state] || 0.05;
return { tax: income * rate, rate };
```
### Sandbox binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"sandbox"` | Yes | -- | Declares this as a sandbox tool |
| `runtime` | `"javascript"` , `"python"` | Yes | -- | Execution runtime |
| `code` | `string` | No | *none* | Inline source code (pipe block syntax for multi-line) |
| `timeout` | `number` | No | Platform default | Maximum execution time in milliseconds |
| `memory_mb` | `number` | No | Platform default | Memory limit in megabytes |
### Isolation
Sandbox tools execute in a gVisor-isolated environment with the following restrictions:
* No network access
* No filesystem access beyond the sandbox working directory
* Resource limits enforced (CPU, memory, execution time)
* Each execution runs in a new environment
### JavaScript runtime
JavaScript code tools have access to standard JavaScript built-ins. The code should return a value matching the declared return type.
```yaml theme={null}
TOOLS:
format_currency(amount: number, currency: string) -> {formatted: string}
type: sandbox
runtime: javascript
code: |
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
});
return { formatted: formatter.format(amount) };
```
### Python runtime
Python code tools use a sandboxed Python interpreter. The code should return a dictionary matching the declared return type.
```yaml theme={null}
TOOLS:
analyze_text(text: string) -> {word_count: number, sentences: number}
type: sandbox
runtime: python
code: |
import re
words = len(text.split())
sentences = len(re.split(r'[.!?]+', text))
return {"word_count": words, "sentences": sentences}
```
***
## Lambda tools
Lambda tools invoke cloud serverless functions. The function name is a logical identifier resolved to an actual endpoint (ARN, URL) at runtime through the project's function registry.
```yaml theme={null}
TOOLS:
process_document(document_url: string, format: string) -> {text: string, pages: number}
description: "Extract text from a document"
type: lambda
function: "document-processor"
runtime: "nodejs20"
timeout: 30000
```
### Lambda binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"lambda"` | Yes | -- | Declares this as a lambda tool |
| `function` | `string` | Yes | -- | Logical function name (resolved at runtime) |
| `runtime` | `string` | No | *none* | Runtime hint (for example. `"nodejs20"`, `"python3.12"`) |
| `timeout` | `number` | No | Platform default | Override timeout for this function in milliseconds |
***
## Async webhook tools
Async webhook tools send an HTTP request with a callback URL injected into the payload. The external system processes the request asynchronously and posts the result back to the callback URL when complete.
```yaml theme={null}
TOOLS:
generate_report(report_type: string, date_range: string) -> {report_url: string, status: string}
description: "Generate a financial report (processed asynchronously)"
type: async_webhook
endpoint: "https://reports.internal/api/generate"
method: POST
callback_url_field: "callbackUrl"
timeout_seconds: 3600
```
### Async webhook binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"async_webhook"` | Yes | -- | Declares this as an async webhook tool |
| `endpoint` | `string` | Yes | -- | URL to send the initial request |
| `method` | `"POST"` , `"PUT"` , `"PATCH"` | Yes | -- | HTTP method |
| `headers` | `Record` | No | *none* | Custom HTTP headers |
| `callback_url_field` | `string` | No | `"callbackUrl"` | Dot-path in the request body where the callback URL is injected |
| `timeout_seconds` | `number` | No | `3600` | Timeout in seconds for the async callback response |
***
## Workflow tools
Workflow tools invoke a durable workflow on the workflow engine. Use these for long-running, stateful orchestrations (waits, polling, human approval, multi-hour processes) that shouldn't run inside the stateless agent runtime.
```yaml theme={null}
TOOLS:
start_onboarding(customer_id: string, plan: string) -> {run_id: string, status: string}
description: "Kick off the customer onboarding workflow"
type: workflow
workflow_id: "customer-onboarding"
mode: async
timeout_ms: 60000
param_mapping:
customerId: customer_id
selectedPlan: plan
```
### Workflow binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"workflow"` | Yes | -- | Declares this as a workflow tool. |
| `workflow_id` | `string` | Yes | -- | Logical workflow identifier. Version routing comes from the deployment manifest. |
| `mode` | `"sync"` , `"async"` | No | `"sync"` | `sync` waits for completion; `async` starts the run and returns a handle. |
| `timeout_ms` | `number` | No | *none* | Maximum time to wait. Supports `{{config.X}}` interpolation. |
| `param_mapping` | `Record` | No | *none* | Maps workflow input fields to tool parameters (workflow key → tool parameter). |
The workflow must exist in the same tenant/project; this is validated at compile time. Legacy `workflow_version` / `trigger_id` fields are no longer used and are silently ignored.
***
## SearchAI tools
SearchAI tools query a SearchAI knowledge base (KB) index, letting an agent ground its answers in the indexed content.
```yaml theme={null}
TOOLS:
search_policies(query: string) -> {results: object[]}
description: "Search the HR policy knowledge base"
type: searchai
index_id: "hr-policies-v2"
kb_name: "HR Policies"
search_instructions: "Prefer the most recent policy version when duplicates exist."
```
### SearchAI binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"searchai"` | Yes | -- | Declares this as a SearchAI tool. |
| `index_id` | `string` | Yes | -- | SearchAI KB index to query. |
| `kb_name` | `string` | No | -- | Display name for the knowledge base. |
| `search_instructions` | `string` | No | -- | Extra instructions injected into the search/classification prompt. |
| `retrieval_profile` | `string` | No | -- | Set to `object-aware` to use object-aware retrieval against the KB index. |
SearchAI tools are tenant-scoped; the tenant binding is applied by the platform. Do not hardcode a `tenant_id` in shared/exported definitions.
***
## Table tools
Table tools perform structured CRUD against an **Agent Table** — a project-, user-, or session-scoped datastore.
```yaml theme={null}
TOOLS:
save_note(title: string, body: string) -> {id: string}
description: "Store a customer note"
type: table
table: customer_notes
scope: end_user
operations: [get, query, insert, update]
```
### Table binding properties
| Property | Type | Required | Default | Description |
| - | - | - | - | - |
| `type` | `"table"` | Yes | -- | Declares this as a table tool. |
| `table` | `string` | Yes | -- | Primary table slug (`^[a-z_][a-z0-9_]*$`). `table_name` is an alias. |
| `scope` | `string` | No | -- | `project`, `end_user`, or `session`. The scope key is derived from trusted session context, never authored in the binding. |
| `operations` | `string[]` | No | read-only (`get`/`query`/`count`) | Allowed operations: `get`, `query`, `count`, `insert`, `update`, `delete`, `upsert`, `bulkInsert`, `bulkUpsert`. |
| `tables` | `string[]` | No | -- | Additional tables to JOIN (max 8). |
| `query_template` | `string` | No | -- | Optional query template. |
***
## Tool file imports
You can organize the tool definitions into reusable `.tools.abl` files and import it into agent documents. This lets you share tool definitions across multiple agents.
### Tool file format
A `.tools.abl` file starts with a `TOOLS:` section that can include shared defaults:
```yaml theme={null}
TOOLS:
base_url: "https://api.hotels.com/v1"
auth: bearer
timeout: 5000
retry: 3
search_hotels(destination: string, checkin: date, checkout: date) -> Hotel[]
type: http
endpoint: "/search"
method: POST
description: "Search available hotels"
get_hotel(hotel_id: string) -> Hotel
type: http
endpoint: "/hotels/{hotel_id}"
method: GET
description: "Get hotel details by ID"
```
### Shared defaults
The following defaults can be set at the top of a `.tools.abl` file and apply to all tools in the file.
| Default | Type | Description |
| - | - | - |
| `base_url` | `string` | Base URL prepended to all relative endpoint paths |
| `auth` | `string` | Default auth type for all tools |
| `timeout` | `number` | Default timeout in milliseconds |
| `retry` | `number` | Default retry count |
| `retry_delay` | `number` | Default retry delay in milliseconds |
| `rate_limit` | `number` | Default rate limit (requests per second) |
| `headers` | `Record` | Default headers applied to all tools |
### Import syntax
Import tools from a `.tools.abl` file into an agent using the `file:` directive within the `TOOLS:` section.
```yaml theme={null}
TOOLS:
file: "./tools/hotels-api.tools.abl" [search_hotels, get_hotel]
```
The import specifies:
* The path to the `.tools.abl` file (relative to the agent file)
* An optional list of specific tool names to import (in brackets). If omitted, all tools from the file are imported.
Project bundle import can also synthesize tool stubs from inline agent `TOOLS:` signatures during apply. That keeps imported agents compileable even when the bundle didn't include companion `.tools.abl` files, and the next export writes those synthesized stubs back out as `tools/.tools.abl`.
### Example
Given the file `tools/hotels-api.tools.abl`.
```yaml expandable=true theme={null}
TOOLS:
base_url: "https://api.hotels.com/v1"
auth: bearer
search_hotels(destination: string, checkin: date, checkout: date) -> Hotel[]
type: http
endpoint: "/search"
method: POST
description: "Search available hotels"
get_hotel(hotel_id: string) -> Hotel
type: http
endpoint: "/hotels/{hotel_id}"
method: GET
description: "Get hotel details by ID"
get_hotel_reviews(hotel_id: string, limit: number = 10) -> Review[]
type: http
endpoint: "/hotels/{hotel_id}/reviews"
method: GET
description: "Get reviews for a hotel"
```
An agent file can import selected tools:
```yaml theme={null}
AGENT: Hotel_Search
TOOLS:
file: "./tools/hotels-api.tools.abl" [search_hotels, get_hotel]
# Additional agent-specific tools
check_availability(hotel_id: string, dates: string) -> {available: boolean}
description: "Check real-time availability"
type: http
endpoint: "/api/availability"
method: POST
```
***
## Tool features
### Confirmation
The `confirmation:` block configures whether the agent should ask the user for confirmation before executing a tool. This is particularly important for tools with side effects (for example. executing a payment, deleting a record).
```yaml theme={null}
TOOLS:
execute_payment(amount: number, recipient: string) -> {confirmation_id: string}
description: "Execute a payment"
type: http
endpoint: "/api/payments"
method: POST
confirmation:
require: always
immutable_params: [recipient]
```
| Property | Type | Default | Description |
| - | - | - | - |
| `require` | `"always"` , `"never"` , `"when_side_effects"` | *none* | When to require user confirmation |
| `immutable_params` | `string[]` | *none* | Parameters that can't be changed after confirmation |
| `consent_required_in` | `"conversation"` , `"explicit_prompt"` | *none* | Where consent must be obtained. |
| `consent_scope` | `string[]` | *none* | Consent scopes required before executing. |
Without a `consent_action`, a required confirmation's question is built from the first sentence of the tool's `description` ("Shall I ``…?"). A `description` that opens with a noun phrase or a third-person verb reads as a broken question (compile warning `TOOL_CONFIRMATION_PHRASE_NOT_ACTION`) — write the description starting with an imperative verb ("Transfer the amount to the beneficiary"), or set `consent_action` explicitly to control the confirmation wording.
\| `consent_action` | `string` | *none* | Named consent action to trigger. |
\| `consent_fallback` | `"explicit_prompt"` , `"block"` | *none* | What to do if consent isn't granted. |
#### What the customer is asked
When a call needs approval, the customer sees a plain-language question in chat and in cascaded voice (speech-to-text, reasoning, text-to-speech). The runtime builds it from the `tool_confirmation_request` message, your `consent_action` (if you set one), and a readable label for each parameter value. The default wording is:
```yaml theme={null}
Please confirm before I go ahead{{action_suffix}}.{{details}}
Shall I proceed?
```
For the `create_order` tool with `consent_action: "create order"`, the customer sees:
```yaml theme={null}
Please confirm before I go ahead (create order).
- Account id: 4411
- Amount: 25
Shall I proceed?
```
* `consent_action` appears in parentheses after "go ahead". Set it to a short, customer-friendly phrase such as "create order". If you leave it out, the parenthetical is omitted.
* Each parameter appears on its own line with a humanized label: `account_id`, `accountId`, and `account-id` all display as "Account id". Empty values are skipped, and values of sensitive gather fields are masked.
* The tool name, parameter lock markers (`immutable_params`), and the instructions the runtime gives the model are never shown to the customer.
* If the tool already ran earlier in the conversation and the agent is about to repeat it, the customer is asked "I already completed this earlier in our conversation (create order). Would you like me to do it again?" instead. Override it with the `tool_confirmation_duplicate_effect` key.
To change the wording, override `tool_confirmation_request` in the agent's [`MESSAGES:`](/agent-platform/abl/reference/lifecycle-and-hooks#messages) block. The template supports `{{action_suffix}}` and `{{details}}`:
```yaml theme={null}
MESSAGES:
tool_confirmation_request: "Before I continue{{action_suffix}}, please check these details:{{details}}\n\nIs that correct?"
```
Realtime speech-to-speech sessions leave approval to the model provider, so this wording does not apply there.
### State authority and provided fields
The `effect` property declares a tool's authority over session state, and `provides` lists the fields the tool is authoritative to supply. These help the runtime reason about which values a tool is allowed to write.
```yaml theme={null}
TOOLS:
set_shipping_address(address: object) -> {saved: boolean}
description: "Persist the customer's shipping address"
type: http
endpoint: "/api/shipping-address"
method: PUT
effect: state_write # read_only | state_write | side_effect
provides: [shipping_address, shipping_zone]
```
| Property | Type | Values | Description |
| - | - | - | - |
| `effect` | `string` | `read_only`, `state_write`, `side_effect` | The tool's authority over external/session state. |
| `provides` | `string[]` | -- | Fields this tool is authoritative to provide. |
`effect` is optional by default, but a project can opt into a **strict realtime effect metadata mode** at compile time. Under that mode, every non-system tool must declare an explicit `effect` — an undeclared one is a compile error (`REALTIME_TOOL_EFFECT_REQUIRED`): *"Tool must declare an explicit effect (read\_only, state\_write, or side\_effect) for strict realtime effect metadata mode."* This lets the compiler safely select the guarded scheduler lane a tool runs on. Strict mode is off by default for backward compatibility; declare `effect` on every tool to stay compatible with it regardless.
When neither `effect` nor `hints.side_effects` is declared, the runtime infers one: for an HTTP tool, `GET`, `HEAD`, and `OPTIONS` default to read-only and every other method defaults to side-effecting; SearchAI and read-only table bindings default to read-only; any other non-HTTP tool defaults conservatively to side-effecting. An explicit `effect`/`side_effects` always wins over the inferred default.
### Result compaction
The per-tool `compaction:` block controls how this tool's results are compacted in conversation history (independent of the agent-wide `EXECUTION.compaction` policy).
```yaml theme={null}
TOOLS:
search_catalog(query: string) -> {items: object[]}
type: http
endpoint: "/api/catalog/search"
method: GET
compaction:
essential_fields: [items.id, items.name, items.price]
max_description_length: 500
summary_timeout_ms: 3000
```
| Property | Type | Description |
| - | - | - |
| `essential_fields` | `string[]` | Fields to preserve verbatim when compacting this tool's result. |
| `max_description_length` | `number` | Maximum characters kept for descriptive fields. |
| `summary_timeout_ms` | `number` | Timeout for LLM summarization of this tool's result. |
| `max_search_tokens` | `number` | Token budget for SearchAI knowledge-base result context sent to the LLM. |
### MODEL\_CONTEXT on a tool result
An optional `MODEL_CONTEXT` block on a tool chooses the view of its result supplied to **later model calls** — independent of [result compaction](#result-compaction) above, which is an automatic LLM-assisted summarization fallback for oversized results. `MODEL_CONTEXT` is an author-declared, deterministic replacement that always applies, regardless of result size.
```yaml theme={null}
TOOLS:
list_products(category: string) -> object
type: http
endpoint: "/api/products"
method: GET
model_context:
fields: [products[].id, products[].name, total]
```
| Property | Type | Description |
| - | - | - |
| `fields` | `string[]` | Result paths to keep, 1-64 entries. Paths may traverse arrays with `[]` (for example `products[].id`); each segment is an identifier, at most 16 segments deep. No path may be a prefix of (or overlap) another. |
| `template` | `string` | A text template (`{{}}` interpolation against `result`) rendered instead of the raw result — for example `"{{result.total}} products found"`. |
`fields` and `template` are mutually exclusive — choose one. The full, unprojected tool result is still available everywhere else: `CALL AS`, `ON_RESULT`, assignments, response templates, and client delivery. Omit `MODEL_CONTEXT` entirely to keep the historical full-result behavior. An invalid or empty policy (both or neither of `fields`/`template`, an empty/overlapping `fields` list, or an empty `template`) fails validation (`MODEL_CONTEXT_INVALID`); if a *valid* policy can't be projected at runtime (a path doesn't resolve, say), the model receives a bounded "unavailable" marker instead of the full result — it never silently falls back to exposing everything.
A rendered, non-empty view is **preserved exactly** through later history compaction and summarization, evaluated once and reused on later turns rather than re-rendered — see [MODEL\_CONTEXT on a response](/agent-platform/abl-reference/flow#model_context-on-a-response) for the full preservation contract and the `AUTHORED_MODEL_CONTEXT_CAPACITY_EXCEEDED` capacity limit, which applies to both forms identically.
Legacy `.agent.abl` format: `MODEL_CONTEXT:` with nested `FIELDS:`/`TEMPLATE:` keys, directly under the tool signature. `MODEL_CONTEXT` can also sit beside a FLOW step's `RESPOND` or a `COMPLETE` condition's `RESPOND` — see [MODEL\_CONTEXT on a response](/agent-platform/abl-reference/flow#model_context-on-a-response) for that (separate) use.
### Input/output transforms (`process`)
The `process:` block defines pure input (`before`) and output (`after`) transform stages that run around the tool call — remapping parameters, enforcing preconditions, and reshaping results.
```yaml theme={null}
TOOLS:
quote_fx(amount: number, from: string, to: string) -> {converted: number}
type: http
endpoint: "/api/fx/quote"
method: POST
process:
before:
params:
amount_cents: "amount * 100"
require: [amount > 0]
on_fail: reask("Please provide a positive amount.")
after:
result:
converted: "result.amount_cents / 100"
on_fail: warn("Could not normalize the quote.")
```
`before` supports `params` (computed parameter map), `require` (precondition expressions), `on_fail` (one of `block("msg")`, `reask("msg")`, `warn("msg")`, `use_original`, `fallback("msg")`), and `use` (a named transform). `after` supports `result` (output remapping) and `on_fail`.
### Caching hints
Use the `cacheable` hint to indicate that a tool's results can be cached.
```yaml theme={null}
TOOLS:
get_exchange_rate(from: string, to: string) -> {rate: number}
description: "Get current exchange rate"
type: http
endpoint: "/api/fx/rate"
method: GET
hints:
cacheable: true
latency: fast
```
When `cacheable: true`, the runtime may cache results for identical parameter combinations. When `cacheable: false`, results are never cached (use this for tools that return time-sensitive data).
### PII access
The `pii_access:` property declares what level of personally identifiable information (PII) a tool can access.
```yaml theme={null}
TOOLS:
verify_identity(ssn_last4: string, dob: string) -> {verified: boolean}
description: "Verify customer identity"
type: http
endpoint: "/api/identity/verify"
method: POST
pii_access: user
```
| Value | Description |
| - | - |
| `original` | Tool can access the original, unredacted PII values (broadest access). |
| `tools` | Tool can access PII data from other tool results |
| `user` | Tool can access user-provided PII |
| `logs` | PII from this tool may appear in logs |
| `llm` | PII from this tool is sent to the LLM |
### Secret scrubbing
An always-on scrubber masks credentials (API keys, bearer tokens, private keys, passwords) in a tool's output. Set `scrub_secrets: false` to opt a specific tool out (for example, when the tool's job is to return a token your flow needs). This is independent of `pii_access`, which governs PII rather than credentials.
```yaml theme={null}
TOOLS:
mint_token(scope: string) -> {token: string}
type: http
endpoint: "/api/token"
method: POST
scrub_secrets: false
```
### Context access
The `context_access:` block declares which session context variables a tool reads from and writes to. This enables the runtime to automatically inject context into HTTP requests and apply result values back to session state.
```yaml theme={null}
TOOLS:
update_profile(name: string) -> {updated: boolean}
description: "Update user profile"
type: http
endpoint: "/api/profile"
method: PUT
context_access:
read: [user.id, user.email]
write: [user.name, user.updated_at]
```
`context_access` is validated against your `MEMORY:` declarations. A tool that declares a `read` of a **write-only** memory path (or a `write` of a **read-only** path) now fails compilation, and at runtime a tool is blocked from executing if its declared memory is unavailable. Declare the matching `MEMORY:` access mode for every path a tool reads or writes.
A `read` key that is only ever populated by this **same tool's own** `on_result`/`on_error` writes can never be satisfied on the tool's first call in a session — that key doesn't exist yet, so the runtime fails the call closed every time. The compiler warns `TOOL_CONTEXT_ACCESS_READ_BEFORE_WRITE` for this. The key needs a producer other than the tool itself: another tool, `ON_START`, a FLOW `SET`, or a `GATHER` field.
### Store result
The `store_result:` property controls whether the raw tool result blob is stored in the session context. Defaults to `true`.
```yaml theme={null}
TOOLS:
log_event(event: string) -> {logged: boolean}
description: "Log an audit event"
type: http
endpoint: "/api/audit"
method: POST
store_result: false
```
### Reuse secrets across tools (SecretRef)
Some flows need a credential produced by one tool (a one-time token, session key, or OTP) to be sent to a later tool — without ever exposing the raw value to the LLM. SecretRef captures such values into an encrypted, session-scoped **secret vault** and restores them into downstream tool calls.
* **Producer** — the tool that returns the secret declares a `RESULT_SECRETS:` block. Each entry captures a result field into the vault under a key, in the form ` -> (options)`. Capture works on **every tool type** — HTTP, code sandbox, and `workflow` tools can all produce vaulted secrets.
* **Consumer** — a downstream tool declares a `SECRET_ACCESS:` block listing the vault keys it may read, then references them with the `{{_secret.}}` placeholder in its `headers` or `body_template`. Only **HTTP tools** can consume secrets — `{{_secret.}}` resolves in the HTTP request path alone.
Declaring `SECRET_ACCESS` on a non-HTTP tool (a `workflow` or code-sandbox tool) is a **compile error** (`SECRET_REF_UNSUPPORTED_CONSUMER_TYPE`): the executor cannot resolve `{{_secret.}}`, so the single-use vault entry would be consumed without delivering the value, and the next authorized consumer would fail with "already used". If you reached for a workflow tool to assemble a variable-length batch payload from a secret, build it directly in an HTTP tool with `{{#each input.}}` in the `body_template` instead. (`RESULT_SECRETS` capture is unaffected and still works on workflow tools.)
```yaml expandable=true theme={null}
TOOLS:
# Producer: capture the OTP token into the vault, never exposing it to the model
request_otp(account_id: string) -> {otp_token: string, expires_in: number}
type: http
endpoint: "/api/otp/request"
method: POST
RESULT_SECRETS:
otp_token -> txn_otp (consumers: [convert_transaction_to_emi], ttl: 300, single_use: true)
# Consumer: restore the vaulted secret into a header — the model never sees it
convert_transaction_to_emi(txn_id: string) -> {status: string}
type: http
endpoint: "/api/txn/convert"
method: POST
SECRET_ACCESS:
READ: [txn_otp]
headers:
X-OTP-Token: "{{_secret.txn_otp}}"
```
#### `RESULT_SECRETS` entry options
| Option | Type | Description |
| - | - | - |
| `` | `string` | Result field to capture (left of `->`). |
| `` | `string` | Vault key to store it under (right of `->`). |
| `consumers` | `string[]` | Tool names allowed to restore this secret. Omit to allow any consumer that declares `SECRET_ACCESS`. |
| `ttl` | `number` | Time-to-live in seconds before the vaulted secret expires. |
| `single_use` | `boolean` | When `true`, the secret is consumed (invalidated) after its first use. |
SecretRef is **fail-closed**: the vault is invisible to the LLM, `{{_secret.X}}` resolves only in `headers` and `body_template` (never forwarded as a query/body param on its own), and an unresolved reference substitutes an empty string — the literal `{{_secret.X}}` placeholder is never sent upstream as a credential.
## Inbuilt tools
Inbuilt tools are tools that the Artemis runtime provides. They cover voice transfers, IVR and DTMF, human-agent handoff, and attachment handling. These tools do not require an HTTP endpoint or a Tool Library entry for them. They are available to all agents by default.
To use these tools, declare them in the `TOOLS:` section of your agent, with its input fields and return type. Then use them as required in the agent.
For the full list of built-in tools and how to use them, see [Inbuilt tools](/agent-platform/inbuilt-tools/index).
***
**Related articles:**
* [Language overview](/agent-platform/abl-reference/language-overview) -- file structure and syntax.
* [Agent declaration](/agent-platform/abl-reference/agent-declaration) -- `tool_timeout` and model settings in EXECUTION.
* [GATHER](/agent-platform/abl-reference/gather) -- information collection (often used alongside tools).
* [FLOW](/agent-platform/abl-reference/flow) -- `CALL` action for invoking tools within flow steps.
* [Inbuilt tools](/agent-platform/inbuilt-tools/index) -- Inbuilt tools.
# Advanced settings
Source: https://koreai.mintlify.app/agent-platform/administration/advanced-settings
The **Advanced Settings** page configures advanced execution parameters for a project. Use it to change the project slug, control LLM reasoning behavior, customize the system prompts that drive data collection, edit escalation messages, snapshot settings versions, and delete the project.
**Navigation**: **Project** -> **Settings** -> **Advanced**
## Slug
The slug is the URL-safe identifier for the project. The platform uses it in project URLs and in any endpoint or integration that references the project by name.
| Field | Description |
| - | - |
| **Slug** | The project identifier. Edit the value and click **Save** to apply the change. |
Editing the slug breaks existing URLs registered in third-party systems. Update every webhook, channel configuration, and external integration that references the old slug before you save.
***
## LLM thinking
Thinking controls whether the LLM produces explicit reasoning before it calls a tool. Reasoning improves traceability in session traces, but it consumes additional tokens on every tool call.
| Setting | Description | Default |
| :- | - | - |
| **Enable Thinking** | When enabled, the LLM provides detailed reasoning (thought) for each tool call. This improves traceability but increases token usage. | Disabled |
| **Thinking Budget** | Maximum number of tokens the LLM can spend on reasoning. Accepts a value from 128 to 32,768. | 1,024 |
| **Thought Description** | Instruction that the platform appends to tool descriptions to guide the LLM's reasoning output. Use it to shape what the model explains, such as why it chose a particular action. | Empty |
**Thinking Budget** and **Thought Description** apply only when you enable **Enable Thinking**. A higher budget produces longer reasoning traces and raises per-call token cost.
***
## LLM task prompts
The platform uses a set of system prompts to handle entity extraction, correction detection, validation, and inference during data collection. Each prompt ships with a default. Expand a prompt row to read the default text, then edit it to change how the model behaves for that task in this project.
| Prompt | Description |
| - | - |
| **Entity Extraction Prompt** | System prompt for extracting field values from user messages during data collection. |
| **Correction Detection Prompt** | Detects when a user corrects a previously provided value. The prompt receives the currently collected values as context. |
| **Field Validation Prompt** | Validates extracted values against field rules and constraints before the platform accepts them. The prompt returns a JSON object that reports whether the value is valid. |
| **Field Inference Prompt** | Infers the most likely values for missing fields from the collected context. The prompt returns a value only when confidence is high. |
The platform marks **Entity Extraction Prompt** as deprecated with inline gather. Projects that use inline gather don't rely on this prompt. Engineering needs to confirm the replacement mechanism and whether the prompt still applies to projects built before inline gather.
Edit these prompts only when the default behavior doesn't fit your data collection flow. Changes apply to every agent in the project.
***
## Escalation templates
Customize the escalation confirmation messages that users see when a conversation hands off to a human agent. Each channel type has its own template:
| Channel | Description |
| - | - |
| **Digital Channel** | Appears in web chat and messaging apps. Supports markdown formatting and emoji. |
| **Voice Channel** | TTS reads it aloud on voice or phone channels. Keep it short and natural. |
| **Plain Text** | Fallback for SMS and channels without rich formatting support. |
Each template supports `{{reason}}` and `{{priority}}` variables that the system substitutes at runtime with the actual escalation reason and priority level.
***
## Danger zone
This archives the project immediately and permanently deletes it after a 30-day grace period. You can restore it from **Archived Projects** before then.
***
## Settings versions
After you change a settings and save it, you can create a versioned snapshot of all project settings. Click **Create Version** to save the current settings state. These versions enable deployment pinning, allowing you to associate a specific settings version with a deployment environment. For example, you can keep tweaking configurations and promote the versioned snapshot to staging deployment to test the project. Once satistied with the configuration, you can promote the specific versioned snapshot to active deployment.
When you [export a project](/agent-platform/projects#export-project), you can include or exclude the version history.
***
# Agent behavior settings
Source: https://koreai.mintlify.app/agent-platform/administration/agent-behavior-settings
Agent behavior settings let you configure behavior profiles, alerts, agent transfer, call recording, API and widget data access, attachments, omnichannel support, and governance policies.
| Settings | Description |
| - | - |
| [Behavior profiles](#behavior-profiles) | Configure overrides for agent speaking, listening, and interaction behavior. |
| [Alert settings](#alert-settings) | Configure rules that trigger notifications when metrics breach thresholds. |
| [Agent transfer](#agent-transfer) | Manage defaults for transfer sessions, routing, voice gateway, and PII handling. |
| [Recording](#recording) | Manage call recordings, playback, retention, and compliance. |
| [Public API Access](#public-api-access) | Configure external API access permissions and integration endpoints. |
| [Public Widget Data](#public-widget-data) | Manage what session and agent data is exposed to widgets. |
| [Attachments](#attachments) | Configure policies for file uploads, storage, and handling in conversations. |
| [Omnichannel](#omnichannel) | Configure multi‑channel support and unified agent behavior across platforms. |
| [Governance](#governance) | Define compliance, audit, and administrative oversight policies. |
## Behavior profiles
The settings on the **Behavior Profiles** page let you define context-driven overrides for agent conversation behavior. Profiles in addition to any agent baseline when the defined conditions match, but don't modify the core definition of the agent.
**Navigation**: **Project** → **Settings** → **Behavior Profiles**
### Create a behavior profile
#### Create profile
Selecting this option opens an editor.
| Field | Description |
| - | - |
| **Profile Name** | A unique identifier for the profile (for example, `voice_support`). |
| **Priority** | Numeric priority. Lower values take precedence when multiple profiles match. |
| **WHEN Expression** | A boolean expression that determines when this profile activates (for example, `true` for always-on, or a context variable check). |
Below the header, the profile editor organizes settings into three behavior sections:
#### Speaking
| Setting | Description |
| - | - |
| **Style** | Response style (for example, warm and concise). |
| **Tone** | Response tone (for example, reassuring). |
| **Emotion** | Emotional register (for example, calm). |
| **Pace** | Response pacing (for example, steady). |
| **Language Policy** | Language selection policy (**Inherit**, **Fixed**, or **Detect**). |
| **Fixed Language** | The language code when **Language Policy** is **Fixed** (for example, `en-US`). |
| **Max Sentences** | Maximum number of sentences per response. |
| **One Thing at a Time** | Whether the agent addresses one topic per turn. |
| **Tool Lead-in** | How the agent introduces tool invocations. |
| **Tool Results Style** | How the agent presents tool results to the user. |
| **Tool Result Points** | Number of key points to extract from tool results. |
| **Internal Handoffs** | Behavior when handing off to another agent. |
| **Human Handoffs** | Behavior when handing off to a human operator. |
#### Listening
| Setting | Description |
| - | - |
| **Barge-In** | How the agent handles user interruptions mid-response. **Inherit** uses the setting configured at a higher level. |
| **On Pause** | Behavior when the user pauses mid-utterance. **Inherit** uses the setting configured at a higher level. |
| **On Overlap** | Behavior when the user and the agent speak at the same time. **Inherit** uses the setting configured at a higher level. |
| **Unclear Audio** | How the agent handles unclear or noisy input. **Inherit** uses the setting configured at a higher level. |
| **Self-Correction** | How the agent detects and handles user self-corrections. **Inherit** uses the setting configured at a higher level. |
| **No-Input Handling** | Behavior when the user doesn't respond within the expected timeout window. |
#### No-input handling configuration
| Field | Description |
| - | - |
| **Enabled** | Turns no-input handling on or off. **Use project default** inherits the setting configured at the project level. [Learn more](/agent-platform/abl-reference/behavior-profiles#voice-turn-taking). |
| **Mode** | Controls how the agent handles no-input retries. **Use project default** inherits the project-level setting. **Dynamic** additionally exposes **Order**, **Play Style**, **Messages**, and **Generation**. |
| **Timeout (ms)** | How long the agent waits for user input before treating the turn as no-input. Default: 5000 ms. |
| **Max Retries** | Number of times the agent re-prompts the user after a no-input timeout before moving to the exhausted behavior. Default: 3. |
| **On Exhausted** | Behavior once **Max Retries** is reached. **Use project default** inherits the setting configured at the project level, **Hangup** ends the conversation, and **Custom** plays a custom message to the user. |
| **Relaxed Interval (ms)** | Timeout duration used for retries after the first, typically longer than the initial timeout to be more lenient. Default: 15000 ms. |
| **Order** | The order in which the agent plays no-input messages across retries (for example, **Random**). |
| **Play Style** | How the agent plays a no-input message to the user. **Use project default** inherits the project-level setting. |
| **Messages** | Select **+ Add message** to configure one or more no-input prompts the agent can play to the user. |
| **Generation** *(Dynamic mode only)* | Configures AI-generated no-input messages, as an alternative or a supplement to authored messages: **Enabled** (turns generated messages on or off, **Inherit** uses a higher-level setting), **Generation Model** (model used to generate the message, **Runtime default model** uses the platform default), **Prompt Library Prompt ID** (ID of the prompt used, runtime uses its active version), and **Max Output Tokens** (maximum length of the generated message, default 150). |
This is only supported for pipeline voice using KoreVG.
**Interaction**
| Setting | Description |
| - | - |
| **Answer Shape** | The structure of responses (for example, direct, exploratory). |
| **Detail** | Level of detail in responses. |
| **Initiative** | How proactively the agent guides the conversation. |
| **Grounding** | How the agent establishes shared understanding. |
| **Clarification** | When and how the agent asks clarifying questions. |
| **Max Clarification Questions** | Maximum number of clarification questions per turn. |
| **Assume When Low Risk** | Whether the agent assumes intent when the risk of misunderstanding is low. |
| **Confirm Parameters** | Whether the agent confirms parameters before executing actions. |
| **Confirm Actions** | Whether the agent confirms actions before performing them. |
| **Uncertainty** | How the agent communicates uncertainty. |
| **Offer Next Steps** | Whether the agent proactively suggests next steps. |
| **Empathy** | Level of empathetic language in responses. |
| **On Correction** | Behavior when the user corrects the agent. |
| **On Confusion** | Behavior when the user appears confused. |
| **On Misheard** | Behavior when the system misrecognizes audio input. |
| **Repair Attempts** | Maximum number of repair attempts before escalation. |
| **Avoid Re-asking** | Whether the agent avoids repeating questions already answered. |
| **Remember Constraints** | Whether the agent tracks and respects stated constraints across turns. |
| **Closure** | How the agent closes a conversation or topic. |
**Authoring Note**: A free-text field for documenting when and why this profile applies.
All settings default to **Inherit**, meaning the agent's base configuration applies. Override any setting to customize behavior for the profile's conditions. Select **Save** to create the profile, or switch to **Raw DSL** to edit the profile definition as code.
***
## Alert settings
The **Alert Settings** page defines rules that trigger notifications when project metrics breach a threshold. The runtime evaluates each rule on a fixed cadence and delivers a notification by email or webhook when the condition matches.
**Navigation**: **Project** → **Settings** → **Alert Settings**
When the project has no rules yet, the page shows an empty state. Select **Create alert rule** to open the rule editor, or **Browse available metrics** to review every metric the evaluator can alert on before you start.
Typical uses include containment rate drops, error spikes, latency increases, and sentiment degradation.
### Create an alert rule
Select **Create alert rule** to open the **Create alert rule** panel. The footer tracks what the rule still needs (for example, `Still needed: name, metric, recipients`) and keeps **Save rule** available once you supply those values. Select **Cancel** to discard the rule.
| Field | Description | Default |
| - | - | - |
| Rule name (required) | A descriptive name for the rule (for example, *LLM latency p95 too high*). | Empty |
| Evaluating | Controls whether the evaluator picks the rule up on its next cycle. Turn it off to keep the rule in the list but paused. | Enabled |
| Subject | The metric family the rule watches: Agent, LLM, Tool, Voice, or Others. The subject determines which metrics the Metric list offers and whether a scope selector appears. | Agent |
| Agent (Agent subject only) | Limits the rule to a single agent. Leave it as *Any agent (project-wide)* to watch the whole project. | Any agent (project-wide) |
| LLM (LLM subject only) | Limits the rule to a single LLM. Leave it as *Any LLM (project-wide)* to watch the whole project. | Any LLM (project-wide) |
| Metric (required) | The metric the rule evaluates. The list reflects the selected subject. Selecting a metric reveals its allowed aggregation and, where supported, a channel filter. | Select a metric |
| Aggregation | The statistical aggregation applied to the metric within the window (for example, avg, p95, sum). Options depend on the selected metric. Only appears once a metric is selected. | Varies by metric |
| Channel | Restricts the rule to a specific channel. Only appears when the selected metric supports channel filtering. | Any channel |
| Condition | The comparison the evaluator applies between the metric value and the threshold (for example, `Less than <` | `Less than <` |
| Threshold (required) | The value the metric must cross for the rule to fire. Use the stepper controls or enter a value directly. May display a unit suffix tied to the metric (for example, *score*). | 0 |
| Window (required) | How far back the rule looks when it evaluates the metric, in minutes. | 10 min |
| Evaluate every (required) | How often the evaluator runs the rule, in minutes. | 10 min |
| Cooldown (required) | How long the rule stays quiet after it fires, in minutes. Use this to suppress repeat notifications for an ongoing condition. | 10 min |
| Deliver via | The delivery channel for the notification: Email or Webhook. | Email |
**If Deliver via : Email:**
| Field | Description | Default |
| - | - | - |
| Recipients (required) | Destinations for the notification. Separate email addresses with commas or new lines. Every recipient receives the alert. | Empty |
**If Deliver via : Webhook:**
| Field | Description | Default |
| - | - | - |
| Webhook URL (required) | Destination URL the platform calls when the rule fires. | Empty |
| Auth profile | Authentication method for the webhook call: `api_key`, `bearer`, or `basic`. | None (no auth header) |
### Subject scopes
| Subject | What the rule watches | Scope selector |
| - | - | - |
| **Agent** | LLM metrics attributed to agent activity. | **Agent**, defaulting to all agents in the project. |
| **LLM** | LLM metrics across the project, independent of the calling agent. | **LLM**, defaulting to all LLMs in the project. |
| **Tool** | Tool execution volume, failures, and latency from platform events. | None. The rule applies project-wide. |
| **Voice** | Voice session, TTS, ASR, and connection metrics from platform events. | None. The rule applies project-wide. |
| **Others** | Trace records and conversation sentiment metrics. | None. The rule applies project-wide. |
### Available metrics
#### Agent and LLM metrics
The **Agent** and **LLM** tabs list the same LLM metrics group. The **Agent** tab scopes each metric to agent activity, and the **LLM** tab scopes it to the LLM. All metrics in this group read from the **LLM metrics** source and support no channel or tool filters.
| Metric | Category | Status | Description | Aggregations |
| - | - | - | - | - |
| **LLM latency** | Operational | Existing | Per-call latency from the LLM metrics stream. | p95, p99, avg, max, min |
| **LLM cost (window total)** | Operational | Existing | Sum of estimated LLM cost over the window. | sum, avg, max, min, p95, p99 |
| **Total tokens** | Operational | Existing | Tokens consumed over the window. | sum, avg, p95, p99, max, min |
| **LLM error rate** | Error | Existing | Share of LLM calls in the window that failed. | |
#### Tool metrics
Tool metrics read from the **Platform events** source. All three support channel filters, channel connection scope, and tool name and type filters.
| Metric | Category | Status | Description | Aggregations |
| - | - | - | - | - |
| **Tool execution count** | Operational | New | Total completed or failed tool calls in the window. | Fixed expression |
| **Tool error count / rate** | Error | New | Failed tool calls. The notification includes failed calls divided by terminal calls. | Fixed expression |
| **Tool latency** | Operational | New | Per-call tool execution latency. | p95, p99, avg, max, min |
#### Voice metrics
Voice metrics read from the **Platform events** source. All support channel filters and channel connection scope, and none support tool name and type filters.
| Metric | Category | Status | Description | Aggregations |
| - | - | - | - | - |
| **Voice session errors** | Error | New | Voice sessions that ended because of a terminal technical error. | Fixed expression |
| **TTS failure count** | Error | New | Text-to-speech operations whose delivery ended in failure. | Fixed expression |
| **Avg ASR confidence** | Quality | New | Average confidence from speech recognitions that reported a real score. | Fixed expression |
| **Voice connection latency** | Operational | New | Time required to establish a voice gateway session. | p95, p99, avg, max |
#### Other metrics
The **Others** tab groups trace and sentiment metrics.
**Trace records**
| Metric | Category | Status | Description | Aggregations | Filters |
| - | - | - | - | - | - |
| **Trace duration** | Operational | Existing | End-to-end trace duration from `spatial_trace_records`. | p95, p99, avg, max, min | None |
| **Slow trace count** | Operational | New | Number of root traces exceeding the configured duration threshold. Takes the `slow_ms` parameter. | Fixed query | None |
**Conversation sentiment**
| Metric | Category | Status | Description | Aggregations | Filters |
| - | - | - | - | - | - |
| **Conversation sentiment** | Quality | Existing | Average sentiment across conversations in the window. | avg, min, max, p95 | Channel filter |
| **Frustration rate** | Quality | Existing | See note | See note | See note |
***
## Agent transfer
The **Agent Transfer Settings** page configures defaults for agent transfer sessions, routing, voice gateway, and PII handling. These settings apply project-wide unless an individual agent or workflow overrides them at runtime.
**Navigation**: **Project** → **Settings** → **Agent Transfer**
The page organizes content into three collapsible sections. Select a section header to expand or collapse it. Select **Save** to apply changes or **Reset** to restore all values to defaults.
| Section | Description |
| - | - |
| **Default Routing** | Default connection, queue, priority, and post-agent behavior. |
| **Voice Gateway** | Voice gateway provider, SIP transfer method, and SIP or PSTN transfer targets. |
| **PII Handling** | PII de-tokenization and detection pattern for transferred messages. |
### Default routing
Default Routing sets the fallback connection, queue, priority, and post-transfer behavior that the platform uses when an agent initiates a transfer. Individual agents or workflows can override these values at runtime.
| Setting | Description | Default |
| - | - | - |
| **Connection** | Agent desktop connection for transfers. Select a configured connection or leave as **None (use agent‑level config)** to defer to the agent's own setting. To add a new connection, select **+**, and select a supported provider from the dialog window. | None (use agent‑level config) |
| **Queue** | Default queue ID for routing. The platform routes transferred conversations to this queue unless the agent specifies a different one. | `default` |
| **Priority** | Default priority level for transferred conversations. Scale: 0 (lowest) to 10 (highest). | 5 |
| **Post-Agent Action** | What happens after the human agent ends the conversation. **Return to bot** hands control back to the AI agent. **End session** terminates the session entirely. | Return to bot |
### Voice Gateway
Voice Gateway configures transfer behavior for voice channels. These settings determine how the platform hands off calls to the agent desktop over SIP or PSTN.
| Setting | Description | Default |
| - | - | - |
| **Gateway Type** | Voice gateway provider. Determines the SIP infrastructure the platform uses for call transfers. | Kore Voice Gateway |
| **Transfer Method** | SIP transfer method for voice handoffs. - **SIP INVITE** - sends a new SIP INVITE to establish a direct call leg with the agent's endpoint. - **SIP REFER** - instructs the caller's endpoint to connect directly to the agent. - **SIP BYE (release-link)** - ends the platform's call leg and releases the caller to a call already linked with the agent. | SIP REFER |
| **Header Passthrough** | Forward custom SIP headers on outbound call transfers. When off, no custom headers are sent. | Enabled |
| **SIP Transfer Target URI** | Project fallback SIP URI used when a call transfer doesn't provide an explicit SIP target (for example, `sip:agent@example.com`). | Empty |
| **PSTN Transfer Target Number** | Phone number in E.164 format (for example, `+14155550123`) dialed when a call transfer uses PSTN. Transferred calls use the provisioned outbound caller ID instead of the original caller's phone number. | Empty |
| **SIP URI is SmartAssist trunk** | Run SmartAssist transcript sync and recording sync after calls transferred to this SIP URI finish. | Disabled |
| **Send Conversation Summary in SIP Header** | Includes an AI-generated conversation summary in the User-to-User (UUI) SIP header when transferring voice calls. Contact centers that support the UUI header can display the summary to agents. This setting is disabled by default and does not affect existing configurations until enabled. | Disabled |
If SIP settings are also defined in the agent DSL, the runtime resolves the transfer destination using the following priority order: the agent-defined default, then the project setting. If neither is available, the transfer fails. Learn how to define [Trusted SIP Transfer Destinations](/agent-platform/abl/reference/tools#trusted-sip-transfer-destinations) in ABL.
### PII handling
PII Handling controls how the platform treats personally identifiable information when transferring a conversation to a human agent.
| Setting | Description | Default |
| - | - | - |
| **De-tokenize Before Transfer** | When enabled, the platform replaces PII tokens with their original plaintext values before sending messages to the agent desktop. Treat this as reveal-class access and enable it only for explicitly authorized destinations. | Enabled |
| **Detection Pattern** | Regex pattern the platform uses to detect PII tokens in messages. The default pattern matches the `{{pii.*?}}` token format. | `\{\{pii\..*?\}\}` |
Enabling **De-tokenize Before Transfer** exposes raw PII to the agent desktop. Only enable this for destinations where human agents have explicit authorization to view personally identifiable information.
***
## Public API access
The **Public API Access** page configures which APIs end-users can access when authenticating through their organization's identity provider (Azure AD, Okta, Google).
**Navigation**: **Project** → **Settings** → **Public API Access**
### Query API
Toggle the **Query API** to allow authenticated end-users to query agents through the public API endpoint. When you enable it, the following configuration fields appear:
| Field | Description |
| - | - |
| **Identity Providers (Auth Profiles)** | Select one or more OIDC-compatible auth profiles (**OAuth 2.0 App** or **Azure AD**). End-users authenticate through their organization's IdP. |
| **Allowed Email Domains** | Comma-separated list of email domains that can authenticate. Leave empty to allow all domains. |
| **Allowed Origins (CORS)** | Optional. A comma-separated list of browser origins that are allowed to make API requests. If this field is not specified, the API does not perform origin validation. If one or more origins are configured, origin validation is enforced for all incoming API requests, and requests from origins not included in the list are rejected. |
| **Allowed Redirect URIs (OAuth Flow)** | Comma-separated full URIs where OAuth redirect responses can go. Exact match only — no wildcards. |
**Session token duration**
| Field | Description | Default |
| - | - | - |
| **Session Token TTL (seconds)** | How long search session tokens remain valid (60–3600 seconds). | 900 (15 min) |
| **Per User (req/min)** | Maximum API requests per user per minute. | — |
| **Per Project (req/min)** | Maximum API requests per project per minute. | — |
***
## Public Widget Data
The **Public Widget Data** page controls the tables, columns, and SQL constructs that saved widgets can expose through the `analytics:read` scope. Projects start with sensitive analytics sources blocked, and all SQL constructs beyond a basic single SELECT stay disallowed until you enable them. Click **Save policy** to apply your changes.
**Navigation**: **Project** -> **Settings** -> **Public Widget Data**
### SQL validation
Choose which saved-widget SQL constructs the platform allows for `analytics:read` queries. DDL and mutation statements remain blocked regardless of these settings.
| Setting | Description |
| - | - |
| **SQL comments** | Allows saved SQL widgets to include comments in query text. |
| **Multiple SELECT statements** | Allows saved SQL widgets to run more than one SELECT, such as set-operation queries. |
| **JOIN clauses** | Allows saved SQL widgets to join allowlisted analytics tables. |
| **UNION, INTERSECT, EXCEPT** | Allows saved SQL widgets to combine allowlisted analytics queries with set operations. |
| **CTE clauses** | Allows saved SQL widgets to use WITH clauses over allowlisted analytics tables. |
| **OR conditions** | Allows saved SQL widgets to use OR inside WHERE clauses. The runtime still applies tenant and project scope outside the expression. |
### Restricted data
The **Restricted data** section lets you block specific analytics tables and columns from saved widgets. Every project contains a default configuration that restricts a few sensitive sources. You can remove a default restriction or add your own. After updating the restrictions, save the policy.
**Block a table**
1. Choose a table from the **Select a table** menu.
2. Click **Block table**.
Blocked tables appear as removable tags. Click the delete icon on a tag to lift the restriction.
The default blocked tables are:
* `abl_platform.audit_events`
* `abl_platform.eval_conversations`
* `abl_platform.eval_scores`
* `abl_platform.hallucination_evaluations`
* `abl_platform.knowledge_gap_evaluations`
* `abl_platform.llm_evaluate`
* `abl_platform.search_queries`
* `abl_platform.spatial_trace_records`
* `abl_platform.custom_pipeline_results`
* `abl_platform.workflow_events`
**Block a column**
1. Choose a scope from the table menu. Select **All tables** to block the column everywhere it appears, or select a specific table to block it there only.
2. Choose a column from the **Select a column** menu.
3. Click **Block column**.
Blocked columns appear as removable tags, grouped by name of the table. The following columns are blocked by default across all tables:
* `attributes`
* `content`
* `conversation_json`
* `evidence`
* `metadata`
* `output`
* `payload`
* `prompt`
* `query_text`
* `trace_events`
***
## Attachments
The **Attachment Settings** page controls how files uploaded during chat sessions are accepted and processed for this project.
**Navigation:** **Project** → **Settings** → **Attachments**
**General**
| Setting | Description | Default |
| - | - | - |
| **Enable Attachments** | Allows users to upload files during chat sessions. Disable this setting to prevent file uploads for the project. | Enabled |
**Upload Limits**
| Setting | Description | Default |
| - | - | - |
| **Maximum File Size** | Maximum size allowed for a single uploaded file. Files larger than this limit are rejected. | 20 MB |
| **Allowed File Types** | MIME types that users can upload. You can configure up to **50** MIME types. | Default types listed below. |
**Default Allowed File Types**
The following MIME types are allowed by default:
* **Images:** `image/jpeg`, `image/png`, `image/gif`, `image/webp`
* **Documents:** `application/pdf`, `application/msword`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/rtf`
* **Spreadsheets:** `application/vnd.ms-excel`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
* **Presentations:** `application/vnd.ms-powerpoint`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`
* **OpenDocument formats:** `application/vnd.oasis.opendocument.text`, `application/vnd.oasis.opendocument.spreadsheet`, `application/vnd.oasis.opendocument.presentation`
* **Text formats:** `text/plain`, `text/markdown`, `text/csv`, `text/tab-separated-values`, `text/html`, `application/xml`, `application/json`
* **Audio:** `audio/mpeg`, `audio/wav`, `audio/webm`
* **Video:** `video/mp4`, `video/webm`
**Processing**
**PII Policy**: Determines how the platform handles files containing Personally Identifiable Information after file contents have been extracted.
* Redact (Default): Detects and masks PII before the extracted content is indexed or made available to downstream services.
* Block: Stops processing the file if PII is detected.
* Allow: Processes the file without applying PII detection or redaction.
PII policies are applied only when the platform extracts file contents (**Full** processing mode). They are not applied in **Metadata Only** or **Skip** mode.
**Default Processing Mode**: Determines how newly uploaded files are processed before they are made available to the agent.
* **Full (Default)**: Processes files and extracts content from supported file types. The extracted content is then shared with the models as context. The models receive content as per the file types.
* Images: A resized version of the image is sent directly to the model as Base64. The model analyzes the image itself. No extraction is done by the platform.
* Documents, audio, and video: The platform extracts the content and sends only the extracted text to the model. The raw file is not sent. In case of audio and video files, the transcripts are shared.
In this mode, File metadata remains available through the attachment tools. Use this mode when the agent needs to read, search, and reason about uploaded content.
* **Metadata Only**: Stores uploaded files without processing their contents. The model cannot access or understand the contents of the uploaded file. The model gets file metadata (name, size, download url).
* **Skip**: Bypasses the platform's content extraction pipeline. This mode is only supported for image and pdf files. For supported types, the platform sends the original file directly to the selected LLM provider. Use this mode when you want to use the LLM provider's native document or vision capabilities instead of the platform's extraction pipeline.
Supported only when the selected LLM provider and model support native file or vision input.
**Points to Note**
* Images in **Full** mode are sent directly to the model. Therefore, the selected model must support **vision**.
* Documents, audio, and video in **Full** mode are converted into text before being sent to the model, allowing any text-capable model to process them.
* **Metadata Only** and **Skip** do not extract file contents. As a result, PII policies are not applied in these modes.
* In **Skip** mode, direct file transfer is currently supported only for **images** and **PDF** files.
**Information**
| Setting | Description | Default |
| - | - | - |
| **Max Files Per Session** | Maximum number of files that can be uploaded during a single chat session. This value is read-only. | 100 |
***
## Omnichannel
The **Omnichannel** page configures cross-channel session continuity.
**Navigation**: **Project** → **Settings** → **Omnichannel**
Omnichannel settings allow users who start a conversation on one channel to continue it on another without losing context.
**Conversation Recall**
| Setting | Description | Default |
| - | - | - |
| **Enable cross-channel recall** | Allow sessions to transfer across channels. | Disabled |
| **Maximum messages to recall** | Number of messages the platform carries over to the new channel. | 20 |
| **Maximum age (days)** | How old a conversation can be and still qualify for recall. | 30 |
**Allowed channels**
Lists all supported channels (**web**, **voice**, **sms**, **whatsapp**, **email**, **slack**, **teams**) with toggles for cross-channel recall participation.
**Identity Requirements**
| Setting | Description | Default |
| - | - | - |
| **Require identity verification** | Whether the system requires identity verification for cross-channel recall. | Enabled |
| **Minimum identity tier** | The minimum identity verification tier required to recall sessions. | 2 - Verified |
**Consent**
| Setting | Description | Default |
| - | - | - |
| **Require explicit consent** | Whether the user must explicitly consent before cross-channel recall. | Enabled |
**Live Transcript Sync**
Configure real-time transcript synchronization settings for cross-channel sessions.
| Setting | Description | Default |
| - | - | - |
| **Enable live sync** | Turns on real-time transcript synchronization across channels during an active session. | Disabled |
| **Join mode** | Controls how a secondary channel joins an in-progress session (for example, **Prompt** asks the user for confirmation before joining). | Prompt |
| **Transcript mode** | Controls what gets synced to the joining channel (for example, **Final Only** syncs only finalized messages, not in-progress ones). Only configurable when **Enable live sync** is on. | Final Only |
**Recent Continuity Activity**
Shows operational events generated by cross-channel recall and live session continuity — for example, a session transferring channels or a live sync joining. Select **Refresh** to reload the list. If no sessions have used continuity features yet, the panel shows an empty state.
Click **Save Settings** to apply changes.
If you see a "Failed to save settings" error, verify that your role has write permissions for project settings.
***
## Governance
The **Governance** page sets the production approval policy for this project. The policy controls how strictly the platform gates production deployments, so a stricter level requires more sign-off before an agent version can ship.
**Navigation**: **Project** → **Settings** → **Governance**
**Production approval policy**
The page shows whether the policy is **Inherited (default)** or set explicitly for the project, along with the **Effective** level currently in force and its source (for example, Standard from the platform default). To set an explicit policy, drag the **Approval strictness** slider to a level, then click **Save policy**.
| Level | Sign-off required |
| - | - |
| **Strict** | The strictest gate. Applies more sign-off than Standard before a version can ship. |
| **Standard** | Requires approval, security, and eval sign-off. The publisher can approve their own version. |
| **Open** | The most permissive gate. Applies less sign-off than Standard. |
When you select a level, the page shows that level's sign-off requirements below the slider. Saving an explicit policy overrides the inherited platform default.
# AI configuration
Source: https://koreai.mintlify.app/agent-platform/administration/ai-configuration
Configure LLM providers, model pricing, Arch settings, voice services, guardrails, and auth profiles.
The **AI Configuration** settings control how your workspace connects to AI models and services. From here you register provider credentials, manage the model catalog, configure safety guardrails, and set up authentication profiles that tools and agents use at runtime.
* **Navigation**: Settings > AI Configuration
* **Required role**: Owner or Admin
***
## LLM providers
The Agent Platform is provider-neutral. Agents can use models from multiple LLM providers, and you can switch between them without changing agent definitions.
* **Navigation**: Settings > AI Configuration > LLM Providers
* **Required role**: Owner or Admin
The page is organized into three tabs: **Credentials**, **Model Catalog**, and **Project Access**.
### Credentials
The **Credentials** tab lists all provider credentials registered in your workspace, grouped by provider. Each credential card shows the name, provider, creation date, and the number of models using that credential.
**Add a credential:**
1. Go to **AI Configuration > LLM Providers > Credentials**.
2. Click **+ Add Credential**.
3. Enter a **Name** (for example, *Production OpenAI*), select a **Provider**, and enter the **API Key**.
4. Click **Add Credential**.
API keys are encrypted at rest and never displayed in plaintext after the initial entry.
**Delete a credential:**
Click the delete icon on the credential card. Deleting a credential disconnects all models linked to it. Those models display a **No Keys** status in the Model Catalog until a new credential is connected.
### Model catalog
The **Model Catalog** tab lists all models registered in your workspace. Each row shows the provider, model name, model ID, linked credentials, and status.
**Workspace tier defaults**
A model becomes a workspace default only when a workspace admin selects it here for a routing tier. It must be ready, active, inference-enabled, and backed by a Workspace-shared connection. Configured models may remain without a default, and a tier default never grants project access.
Defaults are one-per-tier workspace catalog metadata - they don't grant or automatically apply project access. Each project must separately select an authorized binding in **Selected models** (see [Project Access](#project-access)).
For each tier (**Fast**, **Balanced**, **Powerful**, **Voice**), the panel shows the tier's intended use, its associated operation tags (for example Fast: Extraction, Validation, Tool selection), and a **Workspace default** dropdown to assign a model.
Below the tier defaults panel, the model table lists every registered model with its provider, model name and ID, connections, a star (favorite) icon, and **Ready** status.
**Add a model from the catalog:**
1. Go to **AI Configuration > LLM Providers > Model Catalog**.
2. Click **+ Add Model**, browse or search for a provider model.
3. Review and configure the available model settings.
4. Click **Add to Workspace**.
For more information, see [supported models](/agent-platform/supported-models).
For information about configuring models from Microsoft Foundry, see [Microsoft Foundry](/agent-platform/administration/microsoft-foundry).
**Add a custom model**
1. Go to **AI Configuration > LLM Providers > Model Catalog**.
2. Click **+ Add Model**, then select the **Custom Model** tab.
3. Fill in the following fields:
| Field | Description |
| - | - |
| Display Name | A label for the model in the platform. |
| Model ID | The model identifier, for example `gpt-4o` or `claude-sonnet-4-20250514`. |
| Provider | The provider this model belongs to. |
| Tier | Assign the model to a tier: Fast, Balanced, Powerful, or Voice. |
| Temperature | Controls response randomness. Default: 0.7. Available fields vary by the selected model. |
| Max Tokens | Maximum tokens the model can generate per response. Default: 4096. Available fields vary by the selected model. |
| Endpoint URL | Optional. For custom or self-hosted endpoints. |
Available fields in the Custom Model dialog depend on the selected model and provider - not every model exposes Temperature or Max Tokens.
4. Click **Add to Workspace**.
**Configure model settings:**
Expand any model row to view and edit its settings.
**Connections**
Shows credentials linked to this model, with connection visibility and health controls.
* Click **+ Add Key** to connect a credential. In the **Add Connection** dialog, choose one of two connection methods:
| Method | Description |
| - | - |
| **Credentials** | Select an existing credential from the **Credential** dropdown. |
| **Use Auth Profile** | Select an **Auth Profile** instead of a direct credential. Credentials are resolved at runtime via the selected profile, which must have an **Active** status and a compatible auth type for this provider. |
Optionally check **Set as primary connection**, choose **Connection visibility** (default: **Workspace-shared** - the compatibility-safe default; restrict a connection only when its project allowlist is known), then click **Create Connection**.
* Each connected credential is listed with its name, type (for example **API Key**), and whether it's the **Primary** connection. Click **Test** to verify the connection, or the delete icon to remove it.
* Change a connection's **Connection visibility** at any time and click **Save access** to apply.
A model with no credentials shows **No Keys** status and can't serve requests.
**Settings** - Configure Verbosity, Temperature, Top P, Max Completion Tokens, Compaction Threshold, Tier, OpenAI API Mode, and Response Mode. Available fields vary by provider and model capabilities - for example, reasoning models show **Verbosity** instead of Temperature and Top P.
**Reasoning**
For reasoning-capable models, this panel runs diagnostic checks and lets you set the reasoning effort:
| Check | Description |
| - | - |
| **Connection and credentials** | Whether a working, credentialed connection is available. |
| **Boolean thinking** | Whether the model supports a simple on/off thinking mode. Shows **Not applicable** for models that use effort levels instead. |
| **Reasoning effort levels** | Whether the model supports configurable reasoning effort, and which levels it supports (for example: none, minimal, low, medium, high, xhigh). |
Select the **Reasoning Effort** level from the dropdown to control how much reasoning the model applies before responding.
**Capabilities**
Enable supported capabilities: **Default for Tier**, **Tools**, **Vision**, **Streaming**, and **Realtime Voice**. Available capabilities are model-dependent - unsupported ones appear disabled.
Click **Save** to apply changes, or **Delete** to remove the model from the workspace.
### Model tiers
Tiers decouple agent definitions from specific model choices. Agents reference tiers, and the platform resolves the tier to a specific model at runtime:
| Tier | Intended use | Typical models |
| - | - | - |
| Fast | Classification, routing, quick responses. | Claude 3.5 Haiku, GPT-4o mini, Gemini 2.0 Flash. |
| Balanced | General-purpose tasks balancing quality and cost. | Claude 4 Sonnet, GPT-4o, Gemini 2.5 Flash. |
| Powerful | Complex reasoning, analysis, content generation. | Claude 4 Opus, o3, Gemini 2.5 Pro. |
| Voice | Real-time voice interaction. | GPT-4o Realtime, Gemini 2.0 Flash (Live). |
### Project Access
The **Project Access** tab inspects and manages a single project's model bindings and feature routing, from the same authoritative configuration used by Studio and Runtime.
* Select the project from the **Project** dropdown.
* The tab has three sub-tabs: **Selected models**, **Feature config**, and **Resolution diagnostics**.
Secret values and binding identifiers are never shown on this page.
**Selected models**
Shows which workspace-catalog models this project is allowed to use.
* **Model access rollout** banner - indicates whether the project uses selected model bindings only, or has unresolved legacy references.
* **Imported default model unavailable** banner (when applicable) - shows a legacy project default that was imported but can't run until its workspace model and credential are configured, tagged **Project default** and **Workspace model not configured**.
* **Selected models** / **Eligible models** counts.
* The **Selected models** list shows each bound model with its provider, connection type (for example `Workspace-shared`), status (for example **Ready**), and whether it's the **Project default**. Click the **X** to remove a model.
* Click **+ Add models** to open **Add models from the workspace catalog**. Search, then click **Add** next to any workspace-shared or project-restricted connection - each entry shows its provider, connection type, and readiness (for example **Not tested** or **Ready**).
* Use the **Project default (this project only)** dropdown to set which selected model is this project's default, then click **Save selection**.
**Feature config**
Controls how individual platform features (operations) resolve to a model, either by tier or by per-feature override.
* Toggle **Per-operation routing** to switch between "every feature follows its tier" (off) and per-feature overrides (on).
* Banners:
* **Some features need attention** - lists features whose model binding needs a fix.
* **Model access rollout** - same as in Selected models.
* **Catalog features not available for execution** - lists newer catalog features not yet executable (for example: Query rewriting, Retrieval reranking).
* **Tier bindings** - **Fast**, **Balanced**, **Powerful**, and **Voice** cards, each showing the feature count on that tier, a model dropdown (or **No model bound**), and a short description of the tier's intended use. Each tier draws only from this project's selected models.
* Below the tier bindings, the full feature list is grouped by category:
| Category | Purpose | Example features |
| - | - | - |
| **Arch** | Designing, diagnosing, and generating. | Arch assistant |
| **Runtime** | Serving live conversations. | Default runtime model for agents, Entity extraction, Validation, Intent detection |
| **Knowledge** | Answering from your content. | Progressive summarization, Question synthesis, Knowledge graph extraction |
| **Insights** | Computing conversation insights. | Insight intent classification, Insight quality evaluation |
| **Evals** | Testing before release. | Evaluation suite generation, Simulated user, Eval remediation |
| **Pipelines** | Tuning how content is ingested. | Pipeline LLM stages |
Each feature row shows an enabled checkbox, its name, description, and operation code; a tier-or-override control (a tier dropdown, or **Override with a model** to pin a specific model, plus a **Why this model?** link explaining the resolution); which model it **Resolves to**; the source it came **From** (for example **Project default**); and its **Status** (for example **Ready**).
Overrides can use selected project bindings only.
* Click **Save feature config** to apply changes. Select a ready model binding or repair the connection before releasing this project.
**Resolution diagnostics**
A read-only preview of the model and credential source Runtime actually selects for each operation. Secret values and binding identifiers are never shown.
* Choose the **Resolution context** to preview - for example **Project working configuration**, or a specific deployment (for example `dev · 1.0`).
* The table lists each **Operation** (for example Extraction, Validation, Tool selection, Response generation) with:
| Column | Description |
| - | - |
| **Requested Tier** | The tier the operation asks for, and whether it's using the feature's default tier. |
| **Resolves To** | The model and provider that actually serves the operation. |
| **Credential** | Whether a usable credential was found (for example **Credential ready**) and the connection type. |
Click **View resolution details** on a row to see the full precedence trail - for example whether a **Deployment override**, **Project feature override**, **Project model selection**, or **Workspace tier default** was applied.
### Troubleshooting
| Issue | Potential causes | Resolution |
| - | - | - |
| Model shows **No Keys** status | No credential linked to the model. | Expand the model row and click **+ Add Key** in the Connections section. |
| Model validation fails | Invalid API key, incorrect endpoint URL, or blocked outbound connections. | Confirm the API key with the provider directly. For Azure, verify the endpoint includes the deployment name. Check that outbound HTTPS connections to the provider are allowed. |
| Agent can't find a model for the requested tier | No active model assigned to the tier, or the linked credential is inactive. | Verify at least one active model is assigned to the tier with a **Ready** status. Check that the model's linked credential is active. |
| Token budget exceeded | High token consumption across operations. | Review consumption in **Account > Billing & Usage**. Switch high-volume operations to a Fast-tier model to reduce usage. |
***
## Model Pricing
Manage workspace-level LLM pricing overrides and inspect inherited project and platform prices.
* **Navigation**: Settings > AI Configuration > Model Pricing
* **Required role**: Owner or Admin
The page shows four summary counts: **Pricing rows**, **Workspace overrides**, **Project overrides**, and **Platform defaults**.
**Add a workspace pricing override:**
1. Go to **Settings > AI Configuration > Model Pricing**.
2. Click **+ Add pricing**.
3. In the **Add workspace pricing** dialog, enter:
| Field | Description |
| - | - |
| **Provider** *(required)* | The LLM provider, for example `openai`. |
| **Model ID** *(required)* | The model identifier, for example `gpt-5-mini`. |
| **Input price** *(required)* | Price per unit of input tokens. |
| **Output price** *(required)* | Price per unit of output tokens. |
| **Cache read price** *(optional)* | Price per unit for cached-input reads. |
| **Cache write price** *(optional)* | Price per unit for writing to the cache. |
| **Reasoning output price** *(optional)* | Price per unit for reasoning-token output, for models that bill reasoning separately. |
| **Currency** | The currency for all prices on this row. Default: USD. |
| **Effective from** *(required)* | The date this pricing override takes effect. |
| **Effective to** *(optional)* | The date this pricing override stops applying. Leave blank for no end date. |
| **Active override** | Whether this row is used during cost resolution. Inactive rows are retained for history but skipped during resolution. |
| **Notes** *(optional)* | Free-text context, for example a commercial source, approval note, or pricing tier. |
4. Click **Save pricing**.
Workspace overrides apply to every project unless a project-specific override exists.
**Import workspace pricing:**
Use **Import** to bulk-load effective-dated pricing rows, previewing them before committing.
1. Go to **Settings > AI Configuration > Model Pricing**.
2. Click **Import**.
3. In the **Import workspace pricing** dialog, provide the rows either by:
* Clicking **Choose File** to upload a **CSV or JSON file**, or
* Pasting JSON rows or CSV with headers directly into **Import content**.
4. Optionally, check **Allow overlapping windows** to permit pricing rows with overlapping effective-date ranges. Overlapping rows can make effective-price resolution depend on the latest start date.
5. Click **Preview import** to validate the rows before committing.
6. Click **Commit import** to apply the previewed rows as workspace overrides.
Use **Refresh** to reload the page's counts and rows from the latest data.
***
## Arch
Arch is the AI Architect component of the Agent Platform. It manages the model and credentials used for agent specification generation and chat-based orchestration.
* **Navigation**: Settings > AI Configuration > Arch
* **Required role**: Owner or Admin
The page is organized into two tabs: **Settings** and **Audit Logs**.
### Settings
**Current source:**
The **Current Source** banner displays the active credential source and model Arch is currently using, for example *Model Hub · OpenAI · gpt-5.2*.
**Credential source:**
Choose how Arch authenticates with the LLM provider:
| Option | Description |
| - | - |
| Platform Credits | Use platform-provided API credits. Select which model to use in the Settings tab. |
| Model Hub | Use a model already configured with credentials in your Model Hub. |
Arch works best with models that support tool calling and have large context windows.
**Select model:**
Select the model Arch uses from the dropdown. The selected model displays its provider, model ID, capability tags, and tier. If the model hasn't been tested with Arch, a warning appears.
**Parameters:**
Fine-tune generation behavior for chat and spec generation:
| Parameter | Description | Default |
| - | - | - |
| Reasoning Effort | Controls how much reasoning the model applies before responding. Slide between none and high. | Low |
| Verbosity | Controls the length and detail of generated responses. Slide between low and high. | Medium |
| Max completion tokens | Maximum tokens Arch can generate per response. | 6,000 |
| Compaction Threshold | Auto-compaction condenses earlier conversation content once context usage reaches the **Compaction Threshold**, so long-running sessions don't exhaust the model's context window. | |
Click **Save Changes** to apply updates.
### Audit logs
The **Audit Logs** tab shows Arch session activity for the selected time range, including cost, error count, and a browsable session list.
**Summary metrics:**
| Metric | Description |
| - | - |
| Sessions (24h) | Number of Arch sessions in the last 24 hours. |
| Total Cost (24h) | Estimated cost of Arch sessions in the last 24 hours. |
| Errors (24h) | Number of sessions that encountered errors in the last 24 hours. |
| Listed Sessions | Total number of sessions shown in the current session list. |
**Sessions list:**
The list displays all Arch sessions. Use **Has Errors** to show only sessions with errors, or **Today** to show only today's sessions. Each session row shows the session ID, timestamp, number of turns, and current phase. Click a session to view the full execution flow in the detail panel.
### Traces
Use the **Traces** tab on the **Arch** page to inspect execution traces from Arch AI sessions. Traces appear after Arch AI runs an execution with tracing enabled. Until then, the **Sessions** panel shows **No traces yet**.
**Navigation**: **Settings** → **Arch** → **Traces**
The summary cards at the top of the tab show the following:
| Card | Description |
| - | - |
| **Sessions (24h)** | Number of Arch AI sessions in the last 24 hours. |
| **Total Cost (24h)** | Total cost of Arch AI sessions in the last 24 hours. |
| **Errors (24h)** | Number of errors in the last 24 hours. |
| **Listed Sessions** | Number of sessions that match the current filters. |
Use the filters to narrow the session list:
| Filter | Description |
| - | - |
| **Project** | Limits sessions to a single project. Default: **All projects**. |
| **Time range** | Limits sessions to a time window. Default: **Last 24h**. |
| **Phase** | Limits sessions to a phase of the Arch AI workflow: **Interview**, **Blueprint**, or **Build**. Default: **Any phase**. |
The Traces view has three panels:
| Panel | Description |
| - | - |
| **Sessions** | Lists the sessions that match the current filters. Select **Refresh** to reload the list. |
| **Select a session** | Shows the execution tree for the session you select. |
| **Select a span** | Shows the metadata and payload references for the node you select in the trace tree. |
***
## Voice services
Voice Services manages credentials for speech-to-text (STT) and text-to-speech (TTS) providers that power voice-enabled agent interactions.
* **Navigation**: Settings > AI Configuration > Voice Services
* **Required role**: Owner or Admin
Voice testing requires at least one STT (ASR) provider and one TTS provider configured; any supported provider combination works. For Realtime Voice, configure a realtime model (S2S).
### Configure a provider
1. Go to **Settings** > **AI Configuration** > **Voice Services**.
2. Find the provider you want to configure and click **Configure**.
3. Enter the provider credentials in the configuration dialog.
4. Click **Save**.
Configuration fields vary by provider. The following table shows an example for Google Cloud Speech:
| Field | Description |
| - | - |
| Display Name | A label to identify this credential set, for example *Google Cloud Speech Credentials*. |
| Service Account JSON | Paste the full Google service-account JSON used for Speech-to-Text. |
| STT Model ID | Optional. The Google STT model to use, for example `chirp_3`, `chirp`, `latest_long`, `telephony`. Default: `chirp_3`. |
### BYO custom speech providers
Tenant-scoped custom STT/TTS adapters available to voice pipeline channels. Use this when you want to host your own speech recognition or synthesis adapter instead of a built-in provider. You write and run the adapter; the platform routes call audio to it.
Caller audio and transcripts are sent to the endpoints you configure below. Only register endpoints you trust.
**Add a BYO speech provider:**
1. Go to **Settings** > **AI Configuration** > **Voice Services** > **BYO custom speech providers**.
2. Click **+ Add provider**.
3. Enter the provider details:
| Field | Description |
| - | - |
| Display Name | A label to identify this provider, for example *Acme Speech*. |
| Provider slug | A short identifier for the provider. |
| API key | One key for the whole provider. KoreVG sends it as a Bearer token to every enabled endpoint. Encrypted at rest. |
4. Configure the **Speech-to-text (ASR)** and/or **Text-to-speech** adapters (see below).
5. Click **Save**.
Use the **Adapter contract** panel to open the STT or TTS contract while filling in URLs, so you can see what your adapter needs to implement.
**Speech-to-text · ASR**
Streaming recognizer endpoint and optional picker metadata. Toggle **Enabled** to make the adapter available.
| Field | Description |
| - | - |
| STT WebSocket endpoint | The public `wss://` endpoint for your streaming recognizer, for example `wss://speech.example.com/stt`. Private IP ranges are blocked server-side. |
| Languages *(optional, Studio only)* | Languages your adapter supports, for the Studio language picker. Select **+ Add** to add more. |
| STT options (JSON) | Provider-specific options forwarded to your adapter verbatim, for example `{"model":"nova-2","endpointing":300}`. |
**Text-to-speech**
Streaming synthesis endpoint, languages, voices, and options. Toggle **Enabled** to make the adapter available.
| Field | Description |
| - | - |
| TTS streaming WebSocket endpoint | The public `wss://` endpoint for your streaming synthesizer, for example `wss://speech.example.com/tts`. Streaming is the only supported TTS path - batch HTTP TTS is not supported. |
| Languages *(optional, Studio only)* | Languages your adapter supports, for the Studio language picker. Select **+ Add** to add more. |
| Voices *(optional, Studio only)* | Voices your adapter supports, for the Studio voice picker. Select **+ Add** to add more. |
| TTS options (JSON) | Provider-specific options forwarded to your adapter verbatim, for example `{"model":"v2","style":"narration"}`. |
Options entered in the JSON fields are forwarded to your adapter as-is - the adapter decides what they mean.
After saving, verify the adapter with a test call. A registered credential isn't proof the adapter works.
***
## Guardrails
Guardrails provide content safety controls for your workspace and projects. Workspace administrators can configure guardrail providers and their available scanners, while guardrail policies are configured at the project level to evaluate agent inputs and outputs against defined safety rules.
* **Navigation**: Settings > AI Configuration > Guardrails
* **Required role**: Owner or Admin
The page contains two sections: **[Guardrail Providers](#guardrail-providers)** and **[Guardrail Policies](#guardrail-policies)**.
### Guardrail providers
Guardrail providers are the evaluation services that assess content against your policies. Workspace administrators can add and configure guardrail providers for use across projects. Configure at least one provider before creating policies.
Project authors can select which configured providers the project can use. For more information, see [Select guardrail providers](/agent-platform/safety-and-guardrails#select-guardrail-providers).
**Add a provider:**
1. Go to **Settings > AI Configuration > Guardrails**.
2. Click **Add Provider**.
3. In the dialog, configure the provider using the **Form** or **YAML** tab.
4. Click **Add Provider** to save.
**Form tab fields:**
| Field | Description |
| - | - |
| Name | A unique internal identifier for the guardrail provider, for example *OpenAI Moderation*. |
| Display Name | A human-readable label, for example *Content Safety (OpenAI)*. |
| Adapter Type | The integration type for the guardrail provider, such as OpenAI Moderation or Self-hosted / Gateway (OpenAI-compatible). |
| Response format | Available when Self-hosted / Gateway (OpenAI-compatible) is selected. Specifies how the provider response is interpreted: Llama Guard (safe / unsafe + scores), Qwen3Guard (Safety / Categories / Refusal), or Generic classifier (platform prompt → safe / unsafe). |
| Endpoint URL | The provider API endpoint URL. |
| Model | The model used for guardrail evaluation. |
| Authentication | Enable authentication for the provider. Use an Auth Profile for providers that require credentials. Raw API keys aren't accepted. |
| Default Category | The default content category evaluated by the provider. |
| Default Threshold | Sensitivity threshold from 0.0 to 1.0. Default: 0.7. Lower values flag more content; higher values flag only high-confidence violations. |
| Circuit Breaker | Configure failure handling. **Max Failures** sets the number of consecutive provider failures that activates the circuit breaker; **Reset Timeout** sets the time, in milliseconds, before the platform attempts to recover the provider. |
| Retry Configuration | Configure retry behavior. **Max Retries** sets the maximum number of retry attempts when a provider request fails; **Backoff Strategy** sets the delay, in milliseconds, between retry attempts. |
| Enabled | Enable the provider for guardrail evaluation. |
| Synthetic health checks | Enable periodic health checks for the provider using its configured credentials. |
**Configure scanners**
After adding a guardrail provider, workspace administrators can configure the scanners available for that provider.
1. In **Guardrail Providers**, find the provider you want to configure.
2. Click **Scanners** in the provider's **Actions** column.
3. In the **Scanners** panel:
* Add scanners as needed.
* Enable or disable individual scanners.
* Set the threshold for each scanner. A lower threshold is stricter and causes the scanner to trip sooner.
4. Click **Save scanners**.
**Provider health monitoring:**
The platform periodically checks provider health. When a provider becomes unhealthy, its circuit breaker activates. After repeated failures, the circuit breaker opens and stops sending requests. After the reset timeout, it allows a test request through.
When a provider's circuit breaker is open, the platform follows the configured fail mode:
* **Fail-open** - Content is delivered without guardrail evaluation. Violations may go undetected.
* **Fail-closed** - Content is blocked until the provider recovers. Safer, but may interrupt service.
### Guardrail policies
The **Guardrail Policies** section displays the policies configured for a selected project. Select a project from the **Project** dropdown to view its guardrail policies.
Policies are configured at the project level and attached to guardrail providers. For information about creating and configuring guardrail policies, see [Guardrail policies](agent-platform/safety-and-guardrails#guardrail-policies).
***
## Auth profiles
Auth profiles store authentication configurations that agent tools use to call external services securely. Profiles are managed centrally at the workspace level and reused across all projects.
* **Navigation**: Settings > AI Configuration > Auth Profiles
* **Required role**: Owner or Admin
The page displays auth profiles by deployment environment. Use the **Default**, **Dev**, **Staging**, and **Production** tabs to view and manage profiles for each environment.
### Add auth profiles
Click **Add Profile**. Two profile types are available:
| Type | Description |
| - | - |
| Custom Profile | Configure a custom authentication scheme with manually specified credentials and settings. |
| Integration Profile | Configure authentication for a supported third-party integration. |
### Supported auth types
Auth types are organized into four categories.
**Basic authentication**
| Type | Description |
| - | - |
| API Key | Static API key sent via header or query parameter. |
| Bearer Token | Static bearer token in Authorization header. |
| Basic Auth | HTTP Basic authentication with username and password. |
| Custom Header | Send one or more custom auth headers. |
**OAuth 2.0**
| Type | Description |
| - | - |
| OAuth 2.0 App | Authorization code OAuth with PKCE and metadata discovery support. |
| Client Credentials | Machine-to-machine OAuth via client credentials grant. |
| Password Credentials (ROPC) | Legacy OAuth 2.0 Resource Owner Password Credentials (ROPC) flow. Use only when the provider requires resource-owner credentials. |
**Advanced**
| Type | Description |
| - | - |
| Custom Authentication | Describes a provider's authentication scheme when no built-in type fits. |
| AWS IAM (SigV4) | Signs requests with AWS Signature Version 4. |
| Azure AD | Authenticates through Microsoft Entra ID (Azure Active Directory) for enterprise single sign-on (SSO). |
| Kerberos-SPNEGO Auth | Authenticates enterprise systems with SPNEGO/Kerberos. |
| SAML | Uses a SAML assertion-based authentication flow. |
| WS-Security | Authenticates SOAP services with a WS-Security UsernameToken or certificate. |
| GCP Workload Identity Federation | Gives keyless access to Google Cloud services using short-lived tokens. |
| GCP Service Account Key | Exchanges a Google service-account key for short-lived access tokens. |
**Transport security**
| Type | Description |
| - | - |
| mTLS | Mutual TLS with client certificate and private key. |
Auth profile credentials are encrypted at rest and never exposed in logs or agent responses.
Guardrail providers that require credentials must reference an auth profile. Raw API keys are not accepted directly in the guardrail provider configuration.
### Use auth profiles
Once created, reference an auth profile by name in tool configurations and guardrail provider settings. The platform resolves the profile at runtime and injects the correct credentials without exposing them in agent definitions, logs, or traces.
When a tool runs in the Default, Dev, Staging, or Production environment, the platform resolves the auth profile configured for that environment.
# Auth types
Source: https://koreai.mintlify.app/agent-platform/administration/auth-types
## API Key
| Field | Description |
| - | - |
| Profile Name | Required. A label for the profile, for example *API Key - Production*. |
| Description | Optional. Notes on what this profile is for or which service it connects to. |
| Usage Mode | Chooses how the profile is authorized. **Preconfigured** - the admin supplies credentials at setup time, and users don't need to authorize. |
| Visible to project members | Toggle. When on, project members can use this profile. |
| Header Name | Required. The header the key is sent in, for example `X-API-Key`. |
| Placement | Chooses **Header** or **Query Parameter**. |
| Prefix | Optional. Text prepended to the key value. |
| API Key *(Secrets)* | Required. The key value. |
## Bearer Token
| Field | Description |
| - | - |
| Profile Name | Required. A label for the profile, for example *Bearer Token - Production*. |
| Description | Optional. Notes on what this profile is for or which service it connects to. |
| Usage Mode | Chooses how the profile is authorized. **Preconfigured** - the admin supplies credentials at setup time, and users don't need to authorize. |
| Visible to project members | Toggle. When on, project members can use this profile. |
| Authorization Prefix | Optional. Defaults to `Bearer`. |
| Token *(Secrets)* | Required. The bearer token value. |
## Basic Auth
| Field | Description |
| - | - |
| Profile Name | Required. A label for the profile, for example *Basic Auth - Production*. |
| Description | Optional. Notes on what this profile is for or which service it connects to. |
| Usage Mode | Chooses how the profile is authorized. **Preconfigured** - the admin supplies credentials at setup time, and users don't need to authorize. |
| Visible to project members | Toggle. When on, project members can use this profile. |
| Username *(Secrets)* | Required. |
| Password *(Secrets)* | Required. |
## Custom Header
| Field | Description |
| - | - |
| Profile Name | Required. A label for the profile, for example *Custom Header - Production*. |
| Description | Optional. Notes on what this profile is for or which service it connects to. |
| Usage Mode | Chooses how the profile is authorized. **Preconfigured** - the admin supplies credentials at setup time, and users don't need to authorize. |
| Visible to project members | Toggle. When on, project members can use this profile. |
| Headers | One or more header rows. Select **+ Add** to add a row. Each row needs both a header name and a value; values are treated as secrets. |
## OAuth 2.0 App
| Field | Description |
| - | - |
| Profile Name | Required. A label for the profile, for example *OAuth 2.0 App - Production*. |
| Description | Optional. Notes on what this profile is for or which service it connects to. |
| Usage Mode | Chooses how the profile is authorized. **Preconfigured** - the admin supplies credentials at setup time, and users don't need to authorize. OAuth 2.0 App profiles support two additional modes, **JIT** and **Preflight**, which require a project-scoped profile. |
| Scope | Automatically derived from the selected Usage Mode. Defaults to **Shared**. |
| Visible to project members | Toggle. When on, project members can use this profile. |
**OAuth endpoints**
| Field | Description |
| - | - |
| Authorization URL | Required. |
| Token URL | Required. |
| Refresh URL | Optional. The endpoint used to request a new access token when the current one expires. |
| Scopes | Optional. Space-separated scopes, for example `read:users write:tasks offline_access`. Leave empty when the provider doesn't require or advertise scopes. |
| Require PKCE | Toggle. Enable when the provider requires a proof key during authorization, including OAuth 2.1-compatible providers. |
| PKCE Challenge Method | Shown when Require PKCE is on. Defaults to **S256 (Recommended)**. |
| Additional Authorization Parameters | Optional key=value pairs appended to the authorization URL. Select **+ Add** to add a row. |
**Authorized redirect URI**
Copy the generated **Callback URL** and add it to the OAuth provider's authorized redirect URIs (for example, Google Cloud Console or GitHub OAuth Apps).
**Secrets**
| Field | Description |
| - | - |
| Client ID | Required. |
| Client Secret | Required. |
# Catalog settings
Source: https://koreai.mintlify.app/agent-platform/administration/catalog-settings
Catalog settings cover the reusable material a project draws on: response templates, modules imported from elsewhere, modules this project publishes, trace metadata, and telemetry export.
## Templates
The **Template Catalog** holds the response formats agents can send. It's a reference you copy from rather than a place you author. Open a template to see how it renders, adjust its payload, and copy the snippet your agent needs.
**Navigation**: **Project** -> **Settings** -> **Templates**
### Find a template
Search by name, or filter by category. The categories group templates by the job the response does rather than by the channel it targets.
| Category | Description |
| - | - |
| **Content** | Templates carrying formatted text and structured message payloads, including formats specific to a single messaging platform. |
| **Media** | Templates whose payload is a file the client plays, displays, or offers for download. |
| **Data** | Templates presenting numeric or tabular information, where the point is the figures rather than the prose around them. |
| **Input** | Templates collecting a response, where the agent expects the user to act on the message rather than only read it. |
| **Feedback** | Templates gathering a rating or reporting progress back to the user. |
### Reading a template card
Each card carries the template name, a short description, its category, and three badges. The badges are the part to read before you commit to a template, because they tell you whether it will work where your agent actually runs.
| Badge | Description |
| - | - |
| **Web** | Reports how the web chat surface renders the template. A native value means the surface renders the template's own form. Anything else means it renders a substitute, so the agent still responds but the user doesn't see the layout the template describes. Weigh this alongside the channels your agent serves, since a template built for one messaging platform necessarily degrades elsewhere. |
| **Preview** | Reports how the catalog's own preview pane renders the template, which isn't always how the live surface renders it. A limited preview is a limitation of this page rather than of the template, so don't rule a template out on the preview alone. |
| **ABL** | Reports whether you can emit the template from ABL. An insertable template is one you can send directly. A preview-only template can be inspected here but has to reach the user by another route, and a partial value means only some of the template is available. Check this badge first when you're choosing a template to build on. |
### Inspect and use a template
Selecting a card opens a detail panel that takes you from looking at a template to sending one. Its three parts let you see the rendering, change the content, and take away the code.
| Section | Description |
| - | - |
| **Preview** | Renders the template from the payload currently in the editor, so you can see the result before you write any agent code. |
| **JSON Editor** | Holds the template payload, which you can edit to try your own content in place of the sample. A character count below the editor shows how much of the allowed payload size you've used. |
| **ABL Snippet** | Gives the ABL that produces this template. Select **Copy** and paste it into your agent, then replace the sample values. This is the fastest route from choosing a template to sending one, and it saves you deriving the syntax from the payload by hand. |
***
## Imported modules
The **Imported Modules** page lists the reusable components this project has taken from the catalog or from other projects, including agents, tools, and workflows.
**Navigation**: **Project** -> **Settings** -> **Imported Modules**
Each entry records the module name, its version, the project it came from, and when this project imported it. The version and source together are what matter during an upgrade, since they tell you whose change you're taking on and who to ask before you take it.
***
## Module publishing
The **Module Publishing** page turns this project's agents, tools, and workflows into modules other projects can import.
**Navigation**: **Project** -> **Settings** -> **Module Publishing**
Publishing places a module in the workspace catalog, where you choose the resources to include, set the version, and control who can import it. Treat a published module as an interface rather than a copy, because projects that import it inherit your future versions along with the current one.
A release exports the agents, tools, and workflows you selected. Workflows come across in two forms: those you published directly, and those a published workflow tool depends on. The second kind travels whether or not you selected it, so review a release for dependencies you didn't intend to share.
***
## Trace dimensions
The **Trace Dimensions** page defines the custom metadata the platform records with every trace event.
**Navigation**: **Project** -> **Settings** -> **Trace Dimensions**
Each dimension carries a name, a source, and a data type. Dimensions become filters in the session browser and in Insights, so define them around the questions you expect to ask, such as the customer segment or the campaign behind a conversation. A dimension you add today applies to traces recorded from that point on, which makes this a setting worth getting ahead of rather than reaching for once you need the analysis.
***
## Telemetry export
The **Telemetry Export** page sends this project's live model, agent, and tool telemetry to an external observability provider, so agent behavior appears alongside the rest of your infrastructure monitoring.
**Navigation**: **Project** -> **Settings** -> **Telemetry Export**
### Destination
This section holds the provider connection and decides how much telemetry leaves the platform. Complete it before you enable export, since the status switch has nowhere to send to until you do.
| Field | Description |
| - | - |
| **Connection string** | Holds the Azure Monitor connection string, which needs to carry both an instrumentation key and an ingestion endpoint. The field masks the value once saved. |
| **Data export level** | Decides how much leaves the platform. Metadata covers models, token usage, latency, and tool names. Full content adds prompts, responses, and tool inputs and outputs, which makes your observability provider a second store of conversation content and brings it into scope for the same privacy review as the platform itself. |
| **Trace grouping** | Decides what one trace represents. Grouping per session gives you the whole conversation in a single trace with each turn nested inside it, which suits reading conversations end to end. Grouping per turn gives accurate duration and status on each trace, which suits alerting and latency work. |
| **Status** | Turns export to the configured destination on or off without discarding the configuration. |
Provide an Azure Monitor connection string before you save a new telemetry exporter.
### Page actions
Three controls at the foot of the page manage the configuration you've entered.
| Action | Description |
| - | - |
| **Validate** | Checks the destination details you entered. Run it before saving, since an unreachable destination silently drops telemetry rather than reporting an error later. |
| **Save** | Writes the exporter configuration to the project. |
| **Reset** | Returns the form to its last saved state. |
# General settings
Source: https://koreai.mintlify.app/agent-platform/administration/general-settings
General settings govern project membership, authentication keys, model availability, runtime behavior, data lifecycle, compile-time variables, and localization assets.
## Members
The **Members** page controls who reaches your project and what they can do inside it.
**Navigation**: **Project** -> **Settings** -> **Members**
The page lists every project member with their name, email, role, and join date. Select **Add Member** to grant access to an existing workspace member, then assign a role. Removing a member revokes their access at once rather than at their next sign-in.
### Project roles
A role bundles the permissions a member holds everywhere in the project. Assign the narrowest role that still lets someone do their job, since the roles differ less in what they can see than in what they can change.
| Role | Capabilities |
| - | - |
| **Admin** | Manages members, settings, agents, and deployments. Assign it to the people who own the project's configuration, since this role can change anything the other three roles depend on. |
| **Developer** | Creates and modifies agents, tools, workflows, imports, and project resources. This is the working role for anyone building the agent. |
| **Tester** | Reads project resources, creates and reads sessions, runs simulations, and views analytics. It lets someone exercise the agent thoroughly without risking the configuration they're testing. |
| **Viewer** | Reads project resources, sessions, and analytics. Use it for stakeholders who need reporting rather than the build. |
***
## API keys
The **API Keys** page issues and revokes the credentials that carry programmatic access to the project runtime.
**Navigation**: **Project** -> **Settings** -> **API Keys**
| Tab | Description |
| - | - |
| **SDK Keys** | Credentials that client-side SDKs and web widgets present to the project runtime. These travel to browsers and devices, so treat them as public and scope them accordingly. |
| **Platform Keys** | Credentials for server-to-server integrations and administrative API access. These stay on infrastructure you control and carry broader authority than SDK keys. |
Select **Create Key**, name it descriptively, and optionally set an expiration date. Each entry shows the key name, a masked prefix, and the date something last used it. The last-used date is the field that matters during an audit, because it identifies keys you can revoke without breaking anything.
Deleting a key revokes it permanently and immediately. Anything still presenting that key stops working, so confirm the last-used date before you delete.
Issue a separate key for each environment and rotate them on a schedule. A single shared key means one compromise forces you to rotate everywhere at once.
***
## Models
Use the **Models** page to choose which models your project can use and which model handles each platform feature. It has two tabs:
* **Model Configs**: choose the models available to the project.
* **Feature config**: assign models to individual features.
**Navigation**: **Project** -> **Settings** -> **Models**
### Model Configs
Use this tab to select the models your project can use. Only selected models appear in the project's model pickers and feature settings.
The tab shows how many models you've selected out of the models your workspace makes available to the project. Each selected model shows its provider, whether it's **Workspace-shared** or **Project-restricted**, and its status: **Ready**, **Not tested**, **Connection failed**, or **Inactive**.
To change the selection, select **Add models**, choose from the eligible models, and then select **Save selection**. Only project administrators can change the selection. Other roles see the tab as read-only.
If a selected model is no longer available to the project, the tab shows a warning. Select **Remove unavailable models** to clear them, and then replace them as needed.
#### Project default
The project default is the model the project uses when no more specific setting applies. It applies to this project only and doesn't change the workspace.
* **Choose a default.** Use the **Project default (this project only)** field. The default must be one of the selected models. In a project that uses selected models only (see [Model access rollout](#model-access-rollout)), you must choose a default before you can save more than one selected model.
* **New projects.** A new project starts with the workspace's default models selected, and one of them is set as the project default. If the workspace has no default model, the tab shows **No model configured** until you select a model.
* **If the default becomes unavailable.** Features that use the project default switch to the next available selected model. Features assigned directly to a specific model don't switch, and they stop working until you fix or replace that model.
Once a project uses selected models only, the platform never uses a model that isn't selected. The platform doesn't retry a failed request with another model.
#### Model access rollout
The **Model access rollout** banner shows whether your project is limited to its selected models. New projects are limited from the start. Existing projects move over in stages, and the banner shows the current stage.
The banner also shows the number of unresolved references. A warning banner means the project needs repair before it can be limited to selected models. The count isn't an error in your current setup.
### Feature config
Use this tab to assign models to platform features. Different features need different models. Assigning a smaller, faster model to simple tasks and a stronger model to complex ones lowers cost.
The **Per-operation routing** switch turns feature routing on or off for the project. When it's off, every feature uses the project default, and any saved tier models and overrides are kept but ignored. When it's on, each feature uses the model assigned to its tier, so changing a tier's model changes every feature on that tier. You can also assign a specific model to a single feature. Use overrides only when a feature needs something different, because they're easy to forget.
If feature routing is turned off for your whole deployment, the switch is unavailable and the tab shows a notice. Your saved settings are kept and apply again when routing is turned back on.
#### Tiers
Assign one of the project's selected models to each tier.
| Tier | Use it for |
| - | - |
| **Fast** | High-volume, simple tasks where low cost matters most. |
| **Balanced** | Everyday work. |
| **Powerful** | Complex tasks that need the best judgment, at higher cost and slower speed. |
| **Voice** | Real-time speech-to-speech conversations. |
If a tier has no model, its features use the project default. If you configure a tier but no ready model matches it, the features on that tier can't run. The page warns you about missing models and broken connections before you release the project.
#### Features
Features are grouped by area: Arch, Runtime, Knowledge, Insights, Evals, and Pipelines. Each row shows the following:
| Column | Description |
| - | - |
| **Tier or override** | Whether the feature uses its tier's model or a model you assigned directly. You can only assign selected models. |
| **Resolves to** | The model the feature actually uses. Check this column when troubleshooting. |
| **From** | Why that model was chosen: a feature override, a tier, or the project default. |
| **Status** | Whether the feature can run (see the following table). |
| Status | Meaning |
| - | - |
| **Ready** | The feature uses a working model. |
| **Not tested**, **Connection failed**, **Inactive**, **Unknown** | The model's connection isn't confirmed working. Check the model's connection. |
| **Not configured** | No model is assigned. |
| **Off** | The feature is turned off. |
| **Unavailable** | The platform doesn't support running this feature yet. |
Select **Why this model?** on a row to see how the model was chosen. Select **Save feature config** to apply your changes.
***
## Runtime configuration
The **Runtime Configuration** page holds the behavioral controls that govern how the project's agents read input, handle multiple requests in one turn, fill in unstated values, tune voice latency, recover from failures, occupy callers during waits, expose their own activity, protect personal data, and bound their own tool use.
**Navigation**: **Project** -> **Settings** -> **Runtime Config**
Changes here apply to every agent deployment in the project, so treat this page as project-wide policy rather than per-agent tuning. Each group appears as a collapsible card. Select **Save** to apply changes, or **Reset to defaults** to return every setting on the page to its factory value.
### Extraction pipeline
This section decides how the runtime turns a raw user message into structured field values.
| Field | Description |
| - | - |
| **Extraction Strategy** | Chooses which extraction stages run and in what order. Language detection and resolution always run, whatever you choose. The default, **Auto**, runs the stages in sequence: pattern matching runs first, the ML model extracts fields the patterns didn't match, and the LLM extracts anything still missing. Auto gives the best overall accuracy. Change it when your fields sit at one extreme or the other, either structured enough that a model call is waste, or conversational enough that patterns alone leave fields unfilled. The ML stage supports English, French, Spanish, Italian, and Portuguese. For other languages, extraction falls back to the LLM stage. |
| **Confidence Threshold** | Sets the minimum confidence an extracted entity needs before the runtime accepts it. Accepts values from 0.0 to 1.0 inclusive. Default: 0.50. Raise it when the agent accepts values the user didn't give, and lower it when the agent keeps asking for values the user already supplied. |
| **Include Confidence** | Attaches confidence scores to extracted entity values. On by default. Turn it on when downstream logic or analytics needs to see how certain an extraction was. |
### Multi-intent recognition
This section handles messages that ask for more than one thing at once. Without it, the runtime acts on the first request it recognizes and the rest of the message goes unanswered.
| Field | Description |
| - | - |
| **Enable Multi-Intent** | Turns on detection of several intents in one message. On by default. Turn it off for narrow transactional flows, where a second detected intent is more often a misread than a real request. |
| **Strategy** | Sets the order in which the runtime works through detected intents. The choice decides whether the user gets a fast answer to their first request or waits while the agent works the whole message. Default: `primary_queue`. |
| **Unknown Relationship Strategy** | Chooses the fallback for same-turn supervisor routing when relationship data isn't available. Relationship data normally decides ordering, so without it the runtime needs a standing instruction rather than a judgment. Default: `parallel`. |
| **When is a request finished?** | Determines when the runtime considers one customer request complete before moving to the next queued request. Applies only to the `primary_queue` strategy. FLOW agents and agents with **COMPLETE** conditions always finish based on their completion conditions. Default: **When the agent replies**. Recommended: **When the agent confirms it is done**. |
| **Capture every request in one routing decision** | When enabled, the supervisor identifies all user requests during the initial routing decision so additional requests in the same message are not missed. Recommended when using two or more specialist agents. |
| **Max Intents** | Caps how many intents the runtime acts on in one turn. Default: 3. Each additional intent comes from a less certain reading of the message, so raising this raises the chance of acting on something the user didn't ask for. |
| **Confidence Threshold** | Sets the minimum classifier confidence needed to act on a detected intent. Default: 0.6. Raise it when the agent answers requests the user didn't make, and lower it when the agent ignores genuine second requests. |
| **Queue Max Age (ms)** | Sets how long a queued intent stays eligible for handling. Default: 600000 (10 minutes). It stops the agent from returning to a request the user has since abandoned or resolved another way. |
**Multi-intent strategies**
| Value | Behavior |
| - | - |
| `primary_queue` | Handles the first intent immediately and queues the rest, addressing them in sequence once the primary completes. |
| `sequential` | Addresses intents one at a time in detection order, finishing each before starting the next. |
| `parallel` | Dispatches all intents at once and resolves them concurrently. |
| `disambiguate` | Asks the user which request to handle first. |
| `auto` | Lets the runtime choose based on the intent relationship and conversation context. |
**Unknown relationship strategies**
| Value | Behavior |
| - | - |
| `sequential` | Addresses the intents one at a time in detection order. |
| `parallel` | Handles the intents concurrently. |
| `disambiguate` | Asks the user which intent to prioritize. |
[Learn More](/agent-platform/abl-reference/nlu#multi-intent-handling-multi_intent)
### Field inference
This section lets the runtime use an LLM to infer values the user hasn't stated, which shortens conversations at the cost of occasionally guessing wrong.
| Field | Description |
| - | - |
| **Confidence Threshold** | Sets the minimum confidence needed to accept an inferred value. Default: 0.8. Inference trades accuracy for fewer questions, and this field is where you set that trade. |
| **Require User Confirmation** | Presents inferred values to the user before the agent applies them. On by default. Keep it on wherever a wrong inference costs money or changes a record, since confirmation is far cheaper than reversal. |
| **Model Tier** | Selects the tier that serves inference calls. Default: `fast`. Inference runs often and on small decisions, so this choice affects per-turn latency more than most model settings. |
| **Max Fields Per Pass** | Caps how many fields one inference call attempts. Default: 3. Fewer fields per pass means more calls, each of them more reliable. |
### Currency conversion
This section decides where exchange rates come from when an agent quotes or converts an amount. Agents that never handle money can leave it alone.
| Field | Description |
| - | - |
| **Currency Mode** | Selects whether the runtime converts currency using **static** rates held in the platform or **live** rates fetched from an API. Default: `static`. Static rates keep results reproducible across a conversation and across environments. Live rates keep them current. Choose on whether your users compare quotes over time. |
### Reasoning pipeline
The reasoning pipeline runs a lightweight classifier ahead of each reasoning turn so the runtime can route programmatically and narrow the tool set before the main model call.
**Experimental feature. Read before enabling.**
* The pipeline needs a configured `tool_selection` model in your LLM provider settings. Without one, the pipeline skips silently rather than reporting an error.
* It adds one extra LLM call before each reasoning turn, typically 200-500 ms. In parallel mode, the classifier and tool filter run simultaneously.
* If the pipeline model fails three times in a row, a circuit breaker bypasses the pipeline for 60 seconds so the failure doesn't add latency to every turn.
| Field | Description |
| - | - |
| **Enable Pipeline** | Runs the classifier ahead of reasoning to enable short-circuit routing and tool filtering. Everything else in this section depends on it. |
| **Execution Mode** | Decides whether the classifier and tool filter run together or one after the other. **Parallel** runs them simultaneously, which costs less time. **Sequential** waits for the classifier result before deciding whether to run the tool filter at all. Default: `parallel`. |
| **Pipeline Model** | Selects the model that performs intent classification. **Default** uses the project's `tool_selection` model. The pipeline runs on every turn, so this choice sets a recurring latency and cost floor for the whole project. |
| **Short Circuit** | Skips the reasoning loop when classifier confidence is very high. Most of the pipeline's latency saving comes from here, and so does most of its risk, because no reasoning step follows to catch a misclassification. |
| **Short Circuit Threshold** | Sets the minimum confidence (0.0-1.0) a classification needs before the runtime routes programmatically. Default: 0.85. Treat it as the safety margin on short-circuiting. |
| **Tool Filter** | Reduces the tool set sent to the LLM based on classifier analysis. Large catalogs degrade tool selection accuracy, so filtering helps most in projects that expose many tools. |
| **Max Tools** | Caps how many tools reach the reasoning loop. Default: 6. |
| **Keyword Veto** | Overrides classifier routing when specific keywords appear. Use it for terms where you'd rather not depend on a classifier judgment at all. |
| **Veto Keywords** | Holds the comma-separated keywords that trigger the veto, for example `refund, cancel, complaint`. |
| **Intent Bridge** | Maps classifier output to session state so WHEN conditions and multi-intent dispatch can read it. Without it, the classifier's work stays inside the pipeline and your authored logic can't act on it. |
| **Programmatic Threshold** | Sets the confidence needed for Tier 1 actions, where the runtime declines or routes without a model call (auto-decline, auto-route). Default: 0.85. |
| **Guided Threshold** | Sets the confidence needed for Tier 2 guided reasoning, where the runtime keeps the reasoning loop but shapes it by hiding tools and adding routing hints. Default: 0.5. Set it below the programmatic threshold, since guided actions leave the model room to recover and programmatic ones don't. |
| **Out-of-Scope Decline** | Declines out-of-scope queries programmatically, without an LLM call. It removes a model call from every off-topic turn, which pays off most on public channels where off-topic volume runs high. |
| **Multi-Intent Signal** | Injects multi-intent hints into the reasoning turn when the classifier detects two or more intents. |
### Tool call policy
This section bounds the deterministic CALL actions that tool lifecycle hooks invoke. The limits apply only to those hook-driven calls. Side-effecting target tools still run through the normal confirmation flow, so this section doesn't weaken confirmation.
| Field | Description |
| - | - |
| **Boundary CALLs Enabled** | Allows tool lifecycle hooks to invoke another tool deterministically. On by default. Turning it off removes an entire class of chained behavior, so confirm nothing depends on it before you do. |
| **Max CALL Depth** | Caps nested boundary CALL depth in a single turn. Default: 3. The runtime clamps this value to 5 even if you save a higher one, so use this field to set a stricter limit than the platform's own, not a looser one. |
| **Max CALLs Per Turn** | Caps boundary CALL fan-out across one runtime turn. Default: 8. The runtime clamps this value to 20 even if you save a higher one. Fan-out limits matter more than depth limits, because a single wide hook can issue far more calls than a deep chain. |
### Streaming safety
This section sets project-level controls for how the runtime behaves when a safety check intervenes on a response that's already streaming. It's the one safety control where timing matters as much as outcome.
Existing projects stay on **Legacy passthrough** until you change this setting.
| Field | Description |
| - | - |
| **Safety Mode** | Decides how the runtime handles final-output safety interventions for streaming responses. The difficulty is timing: by the time an intervention fires, part of the response may have reached the user. Review the value explicitly rather than assuming the project runs current behavior. |
**Safety modes**
| Value | Behavior |
| - | - |
| **Legacy passthrough** | Keeps the behavior that existed before this setting. Default for projects created before the setting was introduced. |
| **Buffered** | Holds streamed output back until the safety check completes, so nothing unchecked reaches the user. Adds delay before the first words appear. |
| **Gated** | Releases streamed output in stages as each portion passes the safety check. |
| **Terminate forward** | Stops sending further output when an intervention fires. Text already delivered stays with the user. |
| **Retract capable** | Allows the runtime to retract output already delivered when an intervention fires, on channels that support retraction. |
### Output item analytics
Runtime responses often carry repeated structures such as a list of results. This section captures those repeated scalar items so you can run independent analytics queries on them.
Mappings are project-scoped and additive. The runtime captures only the item arrays you configure and the scalar dimensions you declare. It doesn't store raw response JSON in analytics rows.
A counter at the top shows how many mappings you've configured out of the 25 allowed. Select **Add mapping** to create one. Mappings only take effect for responses produced after you add them, so declare a dimension before you need it. You can't recover a field retrospectively.
| Field | Description |
| - | - |
| **Mapping key** | Names the stable analytics category for this item family. Queries group on this key, so keep it stable once collection starts. Renaming it splits your history into two unrelated series. |
| **Item array path** | Absolute response path to the repeated array, such as `$.items[*]`. |
| **Item key path** | Optional relative path, such as `id`, that gives each item a stable identity across responses. Set it when you need to follow the same item over time rather than count occurrences. |
| **Max items** | Caps the rows captured per response for this mapping. Default: 25. It bounds what an unusually large response costs you in analytics writes. |
| **Description** | Optional author-facing note explaining what the repeated item represents. It's the only place a later reader learns why the mapping exists. |
| **Item dimensions** | Declares the scalar fields inside each item that the runtime records. A counter shows how many of the 50 allowed dimensions you've configured. Select **Add dimension** to declare one, each with a name, a path, and a type. The runtime doesn't store fields you don't declare. With no dimensions configured, rows still include the mapping, session, trace, and item metadata. |
Select the trash icon on a mapping to delete it.
### Voice low-latency profile
This section holds channel-scoped defaults for fast voice responses. It tunes model routing, reasoning, context size, and filler behavior for voice while preserving semantic response quality, and explicit developer overrides still take precedence. It appears for projects with a voice channel.
When the project has an active voice channel but the recommended defaults aren't applied, a banner reads **Recommended voice defaults are not active**. Select **Apply recommended defaults** to apply the low-latency profile in one step: fast model routing, inline gather, bounded context, and generative filler defaults.
#### Voice setting differences
The **Voice setting differences** table compares each voice setting's current value with the recommended one, so you can apply the profile wholesale or adopt it setting by setting. Select **Apply** on a row to apply only that recommendation, or **Apply recommended defaults** to apply them all. The rows shown depend on the profile version and on which settings your project already has.
The recommended profile changes these groups of settings:
| Group | What the profile sets |
| - | - |
| Profile | Selects the `customer_service_voice_low_latency_v1` profile. |
| Model routing | Moves `response_gen`, `reasoning`, `coordination`, and `summarization` to the `fast` tier. |
| Reasoning | Turns extended reasoning off (`reasoning.enabled` = `false`) and sets effort to `none`. |
| Execution | Turns on inline gather (`execution.inline_gather`). |
| Compaction | Bounds how much context voice carries into each turn. Covers tool results (`compaction.tool_results`), prior turns (`compaction.prior_turns`), and prompt context (`compaction.prompt_context`). Tool results use structured compaction with a 16000-character maximum and a 4000-character structured threshold. |
| Fillers | Sets how spoken fillers are generated and bounded. Covers the generation strategy, native generative filler, the voice delay, pending-tool status nudges, output limits (tokens and characters), and the generation budget. |
| Session | Controls synchronous session compaction (`session.synchronousCompaction`). It stays off on the live voice path. |
Each row carries a tag that tells you what kind of change it makes:
| Tag | Meaning |
| - | - |
| `latency` | Changes speed or cost only. |
| `semantic_risk` | Can change how the agent reasons, so review these rows individually before you apply them. The reasoning settings carry this tag. |
| `operational` | Changes how the runtime operates rather than how fast it responds or how the agent reasons. |
Applying the recommended defaults overwrites your current values for every row listed. To keep a setting, apply the rows one by one instead.
#### Profile settings
| Field | Description |
| - | - |
| **Profile** | Shows the applied profile name and version, for example `customer_service_voice_low_latency_v1 · v1`. Versioned profile metadata is preserved in project export and import. |
| **Voice model routing** | Sets the model tier for each semantic operation on voice: `response_gen`, `reasoning`, `coordination`, `summarization`, `tool_selection`, `extraction`, and `validation`. Fast is the voice default for semantic operations. `realtime_voice` stays on the voice-capable tier (`voice`). |
| **Reasoning/thinking** | Toggle plus a level dropdown (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). The profile disables extended reasoning by default for latency. Explicit agent and project settings still win. |
| **Inline gather** | Merges entity gathering into the reasoning turn, which removes a separate model call. |
| **Prompt context targets** | Bounds voice prompt context before generation. Default values: 6000 and 16000. Required fields and semantic tool outputs remain subject to tool contracts. |
| **Tool-result compaction** | Uses structured projections first so voice doesn't add a serial summarization call, while preserving essential tool fields. Settings: **Strategy** (`structured`), **Max tool-result chars** (16000), **Structured threshold** (4000), **Recent results to keep** (1), **Tool description chars** (80), and **Summary timeout (ms)** (1500). |
| **Prior-turn compaction** | Keeps a compact semantic preview of prior turns without running synchronous session compaction. Settings: **Strategy** (`compact`) and **Assistant preview chars** (120). |
| **Prompt projection** | Bounds projected context while retaining the current request and complete small structured results. Settings: **Auto tool-result chars** (2000), **Projection strategy** (`preview`), and **Include complete small results** (on). |
| **Generative filler bounds** | Sets the limits for generated fillers. The opener must be ready within the generation budget (at most 2 seconds) or the static fallback plays. Later lines in the same streamed batch can still arrive as follow-ups. Each line must be sayable in about 4 seconds. Defaults: 2000, 160, and 4000. |
#### Filler strategy and coordination
Native speech-to-speech (S2S) fillers are preferred when the provider advertises the capability. Pipeline generation and static speech remain bounded fallbacks.
| Field | Description |
| - | - |
| **Generation source** | Chooses where fillers come from. Default: `s2s_preferred_then_pipeline_llm`, which uses the provider's native filler when available and falls back to a pipeline-generated one. |
| **Earliest voice delay (ms)** | Sets the earliest point at which a voice filler can play. Default: 300. |
| **Follow-up gap (ms)** | Sets the pause between a filler and the next follow-up line. Default: 1200. |
| **Native S2S delay (ms)** | Sets the delay before a native S2S filler plays. Default: 300. |
| **Pending-tool nudges** | Sets how many filler nudges can play while a tool is pending. Default: 1. |
| **Pending-tool deadline (ms)** | Sets how long the runtime waits on a pending tool before nudging. Default: 2000. |
| **Max fillers per turn** | Caps fillers in a single turn. Default: 5. |
| **Allow native generative filler** | Lets the provider's native S2S generate fillers. On by default. |
| **Allow pipeline generative filler** | Lets the pipeline LLM generate fillers when native generation isn't available. On by default. |
| **Filler history messages** | Sets how many recent messages the filler generator sees. Default: 0. |
| **Previous fillers** | Sets how many already-emitted fillers the generator sees, to avoid repetition. Default: 0. |
| **Max output tokens** | Caps the length of a generated filler. Default: 32. |
| **Native S2S filler prompt** | Optional provider instruction for native fillers, for example `Keep the caller informed briefly while the request is processing.` Immutable safety rules remain enforced by the runtime. |
Synchronous session compaction stays disabled on the live voice path. Adjust prompt and tool-result projections instead, so the authoritative turn retains its semantic contract.
### Channel recovery and graceful failure
The Channel Recovery and Graceful Failure policy defines how Runtime responds when model generation, tool execution, provider communication, or voice operations are delayed, interrupted, or fail. It enables channel-specific recovery behavior with bounded recovery, localized messages, sequential model fallback, and safe latency hedging, while preserving conversation state and preventing duplicate or invalid actions.
Recovery is opt-in per channel. Existing channels keep their current behavior until you enable this policy on that channel. Voice uses the voice adapter. Other channels use their native progress and response semantics.
Recovery policies are versioned. Publishing a policy doesn't apply it to any channel until you turn on that channel's opt-in.
| Setting | Description |
| - | - |
| **Policy Status** | Sets whether the policy is in **Draft** or **Published** state, shown alongside the policy version (for example, v1). Runtime reads only published policies. Drafts are stored for review and don't change live sessions. |
| **Default Locale** | Sets the fallback locale for a published recovery message when the caller's locale isn't available. Default: `en-US`. |
| **Channel Opt-in** | Turns recovery on for individual channels: **Voice**, **Web chat**, **SDK/WebSocket**, and **Messaging**. All channels are off by default, and only the channels you turn on apply this policy. |
| **Parent turn deadline (ms)** | Sets the hard upper bound for the complete recoverable interaction. Default: 30000. It's separate from voice filler and TTS budgets. When the deadline is reached, Runtime stops waiting and begins the configured recovery flow. |
| **Progress acknowledgement delay (ms)** | Sets how long Runtime waits before emitting a bounded, channel-appropriate progress update. Default: 1200. This helps reduce perceived silence during recoverable delays. |
| **Recovery retries** | Sets the maximum retry attempts for policy-approved transient failures. Default: 1. Side effects and unknown outcomes are never blindly retried. |
| **Allow progress updates** | Keeps the interaction active with localized progress while authoritative work is pending. On by default. |
| **Enable safe request hedging** | Lets Runtime race duplicate requests to cut latency. Off by default. Runtime only races same-model, streaming, cancellable requests. Unknown or mutating tools fail closed, while platform-certified read-only tools use a plan-only race and execute at most one winner. |
| **Sequential model fallback** | Turns on fallback to alternate model or provider candidates after a recoverable model failure, with a field for how many candidates Runtime may try (default: 1). Runtime selects fallback candidates only after capability, safety, tool-contract, and channel checks. |
| **Model/provider candidates** | JSON array of ordered fallback model or provider candidates. Default: `[]`. Keep the logical model choice separate from provider selection, and mark hedge eligibility only for pure generation. |
| **Localized recovery message catalog** | JSON array of localized, versioned message records that Runtime uses during recoverable delays and failures. Each record has a `key` (for example, `recovery.tool_waiting`), `channel`, `locale`, `text`, `allowedVariables`, `maxCharacters`, `status`, and `version`. Use only allowlisted variables. The catalog rejects internal error codes and executable markup. Runtime picks the message that matches the conversation locale. |
### While the caller waits
This group configures what a caller experiences while the agent thinks or works: spoken filler phrases, background hold-music audio, or both together. The settings interact, so read the whole group before you change any one part.
**How filler settings take effect.** Agent no-input messages and channel-specific Gemini settings take precedence over these project defaults. Otherwise, Runtime uses project filler pools and timing, then localized platform defaults. Native realtime fillers are available only for the `voice_realtime` tier. Genesys realtime supports pending-tool fillers but not the native coordinator filler.
#### Filler settings
Fillers are transient chat and voice status messages the runtime emits while work is in progress. They exist because silence during a long turn reads as a failure, particularly on voice.
When the agent is processing a request, Runtime waits for the configured delay before playing a filler message. Depending on the configuration and channel capabilities, it can play a static filler, generate a contextual filler, or use provider-native realtime fillers until the final response is ready.
| Field | Description |
| - | - |
| **Fillers Enabled** | Master switch for the filler system. With it off, nothing else in this section emits anything. |
| **Chat Fillers** | Shows `status_update` messages in chat clients. |
| **Voice Fillers** | Speaks status updates on voice channels. Voice needs fillers more than chat does, because a chat user can see that something is in progress and a caller can't. |
| **Filler language default** | Sets the fallback language for filler speech when the session has no explicit language. Use a BCP 47 tag such as `en-US`. |
| **Filler language override** | Forces all generated and static fillers to use this language, regardless of the session language. Use a BCP 47 tag such as `en-US`. |
| **Chat Delay** | Sets the delay before static chat fillers are emitted. Default: 1200 ms. Set it above the time a normal turn takes, so fillers mark genuinely slow turns rather than interrupting fast ones. |
| **Voice Delay** | Sets the delay before static voice fillers are emitted. Default: 500 ms. Callers tolerate silence for less time than chat users, so this typically sits below the chat delay. |
| **Follow-up Gap** | Voice only. Sets the pause after a filler finishes playing before the next generated follow-up line (800-3000 ms). Default: 1200 ms. Follow-ups continue until the answer starts or the per-turn limit is reached. |
| **Native realtime turn delay** | Sets the delay before the native Gemini realtime hold phrase. Default: 500 ms. It's independent from normal voice fillers. |
| **Cooldown** | Sets the minimum interval between fillers across turns. Default: 3000 ms. Generated voice follow-ups within one turn are paced by the follow-up gap instead. |
| **Max Per Turn** | Caps fillers emitted during one turn. Default: 5. |
| **Pending-tool status nudges** | Sets the upper bound for native Gemini pending-tool progress messages per turn. Default: 3. Max Per Turn also limits the effective cap. |
| **Contextual filler deadline** | Sets the maximum wait (ms) for a generated pending-tool filler before the runtime uses the fallback, when static fillers are enabled. Default: 500. |
| **Static Fillers** | Uses a deterministic fallback phrase when no contextual filler is available. On voice, a static filler is only ever the opener, and follow-ups are always generated. Turning this off never plays a pre-written line in place of one that failed checks. |
| **Localized static filler pools (JSON)** | Locale-keyed phrase arrays used by chat, voice-pipeline, and native realtime fillers. Default: `{"en": ["One moment."], "fr": ["Un instant."]}`. |
| **Localized status-tag instruction (JSON)** | Configures localized instructions used to generate status-tag-based filler messages. |
| **Status Tags** | Uses the status tags the response model emits as fillers, which lets the model say what it's actually doing instead of emitting a generic wait message. |
| **Generated Fillers** | Enables LLM-generated contextual filler messages instead of static filler text. Runtime uses a lightweight model to generate wait messages that are relevant to the current conversation. This provides more natural progress updates while adding a small amount of processing time during long-running operations. |
| **Include emitted filler in agent context** | Lets later same-turn voice reasoning account for a contextual filler the runtime already emitted. It doesn't add the filler to conversation history, so this changes what the agent knows within the turn rather than what the transcript records. |
| **Filler Model** | Selects the model that generates contextual fillers. |
| **Output Tokens** | Caps the response length for built-in contextual filler generation. Fillers should be short, and a low cap enforces that more reliably than the prompt does. |
| **Prompt Override Tokens** | Caps the response length when a filler prompt override is in use. Overrides usually ask for more than the built-in prompt does, which is why they carry a separate limit. |
| **Conversation History** | Sets how many recent messages the filler prompt includes. More history makes fillers more specific and each generation call more expensive. |
| **Previous Fillers** | Sets how many already-emitted fillers the prompt includes. Its purpose is avoiding repetition within a turn, so it matters most when the max per turn runs high. |
| **Filler Prompt** | Selects a prompt-library version that replaces the built-in filler prompt. Select **Clone built-in** to start from the platform's prompt rather than an empty one. The prompt accepts placeholders for the user message, the conversation history, and previously emitted fillers. |
#### Background audio
Background audio is looped hold music the voice gateway plays during compute waits. It covers silence without the agent having to say anything.
| Field | Description |
| - | - |
| **Background Audio Enabled** | Plays uploaded hold audio while the agent works. |
| **Audio File** | Holds the WAV or MP3 clip the gateway fetches. |
| **Start Delay** | Sets the silence before the audio begins. This is the field that most often stops background audio from working: a spoken filler interrupts the audio, so a filler that arrives first prevents the audio from starting at all. Either lower this delay or raise the spoken filler delay so one has room to begin before the other. |
| **When To Play** | Chooses when background audio plays. Select at least one option. **Between sentences** plays only when barge-in is enabled and the text-to-speech voice supports streaming. When barge-in is disabled, background audio plays only **Before the agent starts responding**. |
#### Greeting audio
Greeting clips play as soon as a call connects, ahead of the agent's first turn. They cover the gap between answer and first response, which is the moment a caller is most likely to think the line is dead.
#### Activity updates
This section sets the visibility policy for what the runtime reveals about its own work. The policy is a ceiling rather than a guarantee, because channels apply only what they support.
The exposure settings deserve particular care. Tool, workflow, and agent names describe your internal architecture, and a name that helps an internal user diagnose a problem tells an external user how your system is built.
| Field | Description |
| - | - |
| **Fillers** | Exposes the status messages generated while runtime work is in progress. |
| **Action Progress** | Exposes progress for memory, knowledge, workflow, tool start, and retry events. It's the safest form of progress reporting, since it says work is happening without saying what. |
| **Tool Preview** | Controls whether tool input and output previews are hidden or redacted. Previews can carry data the user supplied moments ago, so redaction and hiding aren't equivalent choices. |
| **Decisions** | Exposes safe decision labels as action progress. |
| **Thought Summaries** | Exposes model and step thinking summaries when the runtime emits them. Summaries explain reasoning that the agent may not stand behind in its final answer, so exposing them can raise questions the response then contradicts. |
| **System Notices** | Exposes authentication, approval, and waiting-for-input notices. These usually need to stay visible, because they tell the user that the agent is waiting on them rather than working. |
| **Debug Details** | Exposes redacted debug activity meant for internal review. |
| **Expose Tool Names** | Shows tool names in activity messages. |
| **Expose Workflow Names** | Shows workflow and step names. |
| **Expose Agent Names** | Shows agent names where the channel supports it. In multi-agent projects this reveals that a handoff occurred, which reads as competence internally and as confusion externally. |
| **Maximum Text** | Caps visible characters per activity update. |
### Voice no-input defaults
This section sets what happens when a voice caller goes silent. Silence is ambiguous, since it can mean the caller is thinking, has walked away, or didn't hear the prompt, and these settings decide how long the agent waits before treating it as a problem.
| Field | Description |
| - | - |
| **Enabled** | Turns on no-input handling for voice sessions. |
| **Mode** | Chooses between authored prompts and contextual generated prompts. Generated prompts fall back to the authored and default prompts, so choosing them adds a behavior rather than replacing one. |
| **Timeout** | Sets the silence that must pass before a no-input prompt fires. Too short interrupts a caller who's reading something out, and too long lets a caller believe the call has dropped. |
| **Max Retries** | Sets how many no-input prompts the agent issues before it takes the terminal action. |
| **Relaxed Interval** | Sets the extended timeout interval used in dynamic mode. |
| **Play Order** | Sets the order in which no-input messages play. |
| **Play Style** | Sets how messages repeat across retries. Repeating the same prompt signals a system that isn't listening, so this choice affects how a stuck caller reads the failure. |
### Caller backchannel
This section sets the project default for caller backchannel acknowledgements. These are language-neutral sounds, such as "uh-huh", that the runtime treats as acknowledgements rather than interruptions, so the agent keeps speaking. Every agent that doesn't set its own backchannel inherits these defaults.
| Field | Description |
| - | - |
| **Enabled** | Turns on backchannel handling for the project. |
| **Phrases** | Holds the acknowledgement phrases. This is one set for all callers, regardless of language, and the runtime sends it to the voice gateway. Type a phrase and press Enter to add it. |
Backchannel is only effective with at least one phrase. If you turn it on without adding any, the page shows the warning **Backchannel is on but has no phrases**.
### On hold
This section sets the project default for phrases a caller says to make the agent pause, such as asking it to wait a moment. Phrases are locale-aware. Every agent that doesn't set its own on-hold behavior inherits these defaults. On hold runs entirely in the runtime and needs no gateway change.
| Field | Description |
| - | - |
| **Enabled** | Turns on on-hold detection for the project. |
| **All locales (base)** | Holds phrases that apply in every language. Use it for phrases that work regardless of locale. |
| **Add language** | Adds a language-specific phrase list. Select a language, then add the phrases callers use to ask for a pause in that language. |
| **Grace (ms)** | Sets the grace period that applies to a hold. Default: 30000. |
| **Max holds** | Sets the maximum number of holds allowed. Default: 2. |
| **Suppress reply** | Controls whether the agent suppresses its reply when it detects an on-hold phrase. Off by default. |
On hold is only effective with at least one phrase. If you turn it on without adding phrases in any language, the page shows the warning **On-hold is on but has no phrases in any language**.
### Barge-in continuation window
When a caller interrupts (barges in) and speech recognition splits their sentence into separate phrase-finals, this section makes the runtime hold each one briefly and merge any follow-on phrase into a single turn. Without it, the agent would treat each fragment as its own turn and may answer a half-finished sentence. This is a project-wide default, and an agent can override it.
| Field | Description |
| - | - |
| **Enabled** | Merges segmented barge-in speech into one turn. On by default. Turn it off to commit each phrase immediately, which is the legacy behavior. |
| **Continuation window (ms)** | Sets how long the runtime holds a barge-in final while waiting for a continuation before it commits. Range: 300-2000 ms. Default: 700. A longer window catches slower speakers but delays the agent's response. |
| **Maximum hold (ms)** | Sets the absolute ceiling on how long one turn can be held, regardless of merges. Range: 500-8000 ms. Default: 2500. |
| **Maximum merges** | Sets the most phrase-finals the runtime folds into one committed turn. Range: 1-20. Default: 4. |
| **Maximum characters** | Sets the most characters of merged transcript before the runtime forces a commit. Range: 1-20000. Default: 2000. |
### Voice speech recognition
These are project-wide defaults. A channel connection can override them, so check the connection before you conclude the project value is what's running.
| Field | Description |
| - | - |
| **Default minimum STT confidence** | Rejects transcripts below this confidence and treats them as no-input. Confidence scores are provider-relative rather than absolute, so a value that works for one speech provider won't carry over to another. Calibrate in staging before you set it in production, and set it to zero to disable the gate entirely. |
### PII redaction
This section controls how the runtime handles personally identifiable information in conversations. Baseline credential and secret scrubbing protects logs and read APIs whether or not PII protection is on, so the settings here extend that protection rather than establishing it.
| Field | Description |
| - | - |
| **Enable PII Protection** | Detects and redacts PII in conversations. It's the switch the rest of this section depends on. |
| **Redact Input** | Redacts PII in user messages before they reach the model. Redaction is context-aware and exempts fields the agent is actively gathering, so turning it on doesn't break a flow that legitimately asks for a phone number. |
| **Redact Output** | Redacts PII in agent responses before delivery. Its purpose is stopping the agent from echoing back PII the user already provided, which is a common way collected data reaches a transcript. |
| **Classify explicit DTMF using project PII patterns** | Matches a completed keypad entry to the project's enabled PII patterns, so a recognized value carries a type and a validation status while anything else stays a generic token. Raw digits never reach the model either way, so this setting changes how an entry is labelled rather than whether it's protected. |
| **Detection methods allowed** | Sets the ceiling on which detection engines the project's patterns may use. Each pattern still selects its own method on the PII Protection page, so this field decides what's available to them rather than what they do. |
### Lookup tables
Lookup tables are the reference data the runtime uses to validate field values and perform fuzzy matching during extraction.
Project lookup tables defined here are the canonical source for gather validation and fuzzy matching. Agent-local `LOOKUP_TABLES` remain available only as an experimental compatibility lane, so build new work on project tables.
| Field | Description |
| - | - |
| **Table Name** | Names the table for reference from field validation. |
| **Source** | Selects where the table's values come from, which decides whether you maintain them here or elsewhere. |
| **Values** | Holds the comma-separated values for an inline table. |
| **Case Sensitive** | Requires an exact case match. Leave it off for anything a user types, since typed input rarely matches your stored casing. |
| **Fuzzy Match** | Accepts near matches. It rescues misspellings and transcription errors at the cost of occasionally accepting a value the user didn't mean, so it suits voice input more than typed input. |
***
## Data retention
The **Data Retention** page sets how long evaluation and scoring data stays available to this project.
**Navigation**: **Project** -> **Settings** -> **Data Retention**
Retention is a deletion policy rather than a storage preference. Once a window passes, the data is gone and no longer available for trend analysis, so set these values around the longest comparison you expect to run rather than the storage you expect to use.
| Category | Description |
| - | - |
| **Eval conversations** | Holds evaluation conversation transcripts. Transcripts are what let you re-read a failed evaluation rather than only seeing that it failed. |
| **Eval scores** | Holds evaluation score records. Scores outlive their usefulness more slowly than transcripts, since trend comparison needs the numbers rather than the conversations. |
| **Production scores** | Holds production scoring data, which is the basis for the quality trends in Insights. |
| **Synthetic eval runs** | Holds synthetic evaluation run data. These runs are reproducible on demand, which is why they carry the shortest window. |
The **Retention Windows** section below the summary sets the time-to-live in days for each category. Select **Save** to apply changes or **Reset** to restore defaults.
***
## Config variables
The **Config Variables** page defines key-value pairs that the platform resolves at compile time through `{{config.KEY}}` syntax.
**Navigation**: **Project** -> **Settings** -> **Config Variables**
Use config variables for values that several agents share and that change between environments, such as endpoints, feature flags, and persona-specific content. The alternative is repeating the value in each agent, which turns an endpoint change into an audit.
### Add a variable
Each variable is one key the platform substitutes wherever your agents reference it. The fields below define the key, its value, and the context a later reader needs.
Select **Add Variable**, then complete the fields.
| Field | Description |
| - | - |
| **Key** | Uniquely identifies the variable and forms the reference used in code. Changing a key later breaks every reference to it, so settle naming before the value spreads. |
| **Value** | Holds what the platform substitutes at compile time. You can override it per deployment environment, which is the mechanism that lets one project target different endpoints. |
| **Namespaces** | Groups related variables so a large set stays navigable. |
| **Description** | Explains what the variable is for. It's the only context a later reader gets when deciding whether a variable is safe to change. |
Config variables resolve at compile time rather than at runtime, so a changed value reaches your agents only after recompilation.
***
## Localization
The **Localization** page manages locale JSON assets in Studio and syncs them through the project's Git integration.
**Navigation**: **Project** -> **Settings** -> **Localization**
The platform stores locale assets as project-level JSON files and exports them to Git under the same path contract that project import and export use. Keeping paths in the canonical shape is what lets assets round-trip cleanly, so a path you improvise here becomes a merge problem later.
Counters at the top report the assets, locales, and shared assets the project holds. A status indicator reports when Git isn't connected, with a link to **Open Git Settings**. Until you connect Git, assets stay in project storage and don't reach version control, which means no diffs and no review.
Search by locale, file path, or description, and narrow the list by locale or by scope when the asset count grows past browsing.
### Create a localization asset
An asset is one locale JSON file holding translated strings, either shared across the project or scoped to a single agent. Creating one here puts it under the same path contract Git and project export use.
Select **New Asset** to open the editor, then complete the fields and select **Create**. Saving writes the asset into project storage and keeps the Git export path aligned.
| Field | Description |
| - | - |
| **Relative Path** | Sets the locale file path in the canonical locale and asset shape. This path determines where the asset lands in Git and whether import and export find it again. The path also indicates whether the asset is shared by all agents or used by a specific agent. |
| **Description** | Gives translators and reviewers the context the JSON itself doesn't carry. |
| **JSON Editor** | Edits the locale content directly. Select **Upload JSON** to bring in an existing file, or **Prettify** to reformat what's there. |
* **For shared messages** : Use a shared locale asset for messages that can be used by multiple agents. For example: 'locales/en/\_shared.json\`.
* **For Agent specific messages**: Use an agent-specific locale asset for messages used by a particular agent. For example: 'locales/en/agent\_name.json`, where `agent\_name\` is the name of the agent which can use this asset.
### Use Localized Messages in ABL
Reference localized messages in ABL using the `{{locale.}}` syntax.
```yaml theme={null}
AGENT: SupportBot
FLOW: main
LISTEN
RESPOND: {{locale.greeting}}
LISTEN
RESPOND: {{locale.menu}}
```
The same localization key retrieves the message for the resolved language.
# Integrations settings
Source: https://koreai.mintlify.app/agent-platform/administration/integrations-settings
Integrations connect your project to the external systems it depends on: a Git repository for version control, and the authentication credentials that let agents reach third-party services.
## Git integration
The **Git** page connects a repository so agents and localization assets live under version control rather than only in Studio.
**Navigation**: **Project** -> **Settings** -> **Git**
### Connect a repository
Select **Connect Repository**, complete the fields, then select **Test connection**. Test before you connect, because a wrong branch, path, or credential fails at the first sync rather than at connection time. Select **Connect** to finish.
| Field | Description |
| - | - |
| **Provider** | Selects the Git hosting service. It determines which credential types the **Auth Profile** picker accepts, so set it before you prepare a credential. |
| **Repository URL** | Points at the repository over HTTPS or SSH. It addresses the repository only. The folder this project occupies inside it comes from **Sync Path** further down. |
| **Repository Layout** | Decides whether this project shares a repository with others or takes one of its own. Choose a shared folder when you want every project's history in one place and one set of repository permissions. Choose a dedicated repository when a project needs its own access control or its own release cadence. |
| **Branch** | Sets the branch the project reads and writes. Point non-production projects at a branch other than the one your builds run from, so an experiment in Studio doesn't reach it. |
| **Sync Path** | Sets the folder inside the repository that this project owns. Use the repository root when the project has the repository to itself, and a named folder when several projects share one. |
| **Path Preview** | Shows the location the platform resolved from your layout, path, and branch. Read it before you connect. It's the one place the three fields appear as the single destination they combine into. |
| **Auth Profile** | Selects the stored credential the sync uses. Select **Create Git token profile** to build one without leaving the dialog. |
| **Conflict Strategy** | Decides what happens when Studio and the repository both change the same thing. Settle this before your first push rather than during your first conflict, since the strategy determines which side loses work. |
### Credentials by provider
The picker lists only profile types that carry a token or key the sync can present on its own. OAuth App profiles never appear, whichever provider you choose, because they authorize tools and connectors interactively and the sync has no user to prompt. A working OAuth App profile for the same provider is therefore not a substitute, and its absence from the list isn't a fault.
| Provider | What to prepare |
| - | - |
| **GitHub** | A Bearer Token profile holding a personal access token, or an SSH Key profile. |
| **GitLab** | A Bearer Token or API Key profile holding a project access token, or an SSH Key profile. OAuth Token grants don't appear here either. |
| **Bitbucket** | A raw repository or workspace access token, or an SSH Key profile. Atlassian API tokens need storing in the combined email and token form rather than as the token alone, which is the most common reason a valid Bitbucket credential fails. OAuth Token grants don't appear here either. |
| **Azure DevOps** | A Bearer Token or API Key profile holding an Azure DevOps personal access token. Grant it Code read and write, add Code manage if you want branch and pull request operations, and add a webhook subscription scope if you want automatic sync. SSH Key and OAuth profiles aren't supported for this provider at all, so plan on a token. |
## Once the integration is live, you can push agent definitions, pull repository changes into Studio, and resolve conflicts when edits collide.
## Auth profiles
An auth profile is a reusable credential that connectors, tools, and integrations reference by name. Defining credentials here rather than inside each tool means a rotation touches one profile instead of every tool that uses it.
**Navigation**: **Project** -> **Settings** -> **Auth Profiles**
### Project and workspace profiles
Every profile carries a scope, and the scope decides where it's managed and who can change it. Check it before you plan any credential work, because some of the profiles in this list aren't yours to edit.
| Scope | Description |
| - | - |
| **Workspace** | Credentials the tenant shares across every project. They appear in this list so you can see what's available, but the actions column offers only **Manage in Workspace**. Edit them under workspace administration, where a change reaches every project at once. |
| **Project** | Credentials this project owns. Only these carry the enable toggle, edit, and overflow actions in the project list. |
The distinction decides who can act. If a workspace profile needs different credentials for one project, create a project profile rather than asking for the workspace one to change, since editing the shared profile affects projects you don't own.
### Find a profile
Environment tabs across the top narrow the list by environment, and each tab carries the number of profiles that resolve for it. Filters on the right narrow by status, authentication type, and source, and a search box matches on profile name.
The counts differ between tabs because environments inherit credentials they don't define. A profile appears under an environment whether it holds credentials specific to that environment or falls back to the default set.
### Profile list
The list holds every profile this project can reach, whether it owns them or inherits them from the workspace. The columns tell you what each profile authenticates, where it's managed, and whether it still works.
| Column | Description |
| - | - |
| **Name** | Identifies the profile. This is the name tools reference, so renaming affects everything bound to it. |
| **Auth Type** | Reports the authentication mechanism the profile implements. |
| **Scope** | Reports whether the workspace or this project owns the profile, and therefore where you can edit it. |
| **Owner** | Names the person who created the profile. It's the first thing to check before you disable a credential you didn't set up. |
| **Created At** | Dates the profile. Useful when you're auditing which credentials predate a rotation policy. |
| **Status** | Reports whether the profile is active or revoked. A revoked profile stays in the list rather than disappearing, so anything still referencing it fails with a traceable cause rather than a missing reference. |
### Create a profile
Select **New Profile** and choose one of two routes. The route decides how much you configure by hand and how much the platform already knows.
| Route | When to use it |
| - | - |
| **Integration Profile** | The service you're connecting to is one the platform already supports. The integration carries the authentication mechanisms that service accepts, so you supply credentials rather than working out how the provider expects to be authenticated. |
| **Custom Profile** | The service isn't in the integration list, or you need a mechanism the integration doesn't cover. You choose the authentication type yourself and complete every field. |
Prefer the integration route where it's available. It removes the part of this task most likely to go wrong, which is matching a provider to the right authentication mechanism and endpoint set.
### Integration profiles
The integration picker lists the services the platform supports, grouped by what they're for: communication, productivity, storage, CRM and sales, service management, AI and development, and document processing.
Each tile names the service, the authentication types that service accepts, and how many profiles the project already holds for it. Read the profile count first, because an existing profile is usually a credential to reuse rather than a duplicate to create.
The tiles also tell you what you're in for before you commit. Some services accept a single authentication type, some accept several and leave the choice to you, and a few need no credential at all.
Search by name when the service you want isn't in view, then select **Continue**.
### Custom profiles
Choosing **Custom Profile** takes you to the authentication type selector. Choose the type the provider requires. The type determines the configuration and secret fields you complete next, and you can't change the fundamental mechanism afterward without creating a new profile.
**Basic authentication**
| Type | When to choose it |
| - | - |
| **API Key** | The provider issues a long-lived static key and expects it on every request. It's the simplest type and handles no expiry or refresh, so rotation stays entirely your responsibility. |
| **Bearer Token** | The provider issues a static token that belongs in the Authorization header. It differs from API Key mainly in placement, so follow what the provider's documentation specifies. |
| **Basic Auth** | The provider still accepts a username and password pair. These credentials rarely expire on their own, which makes them the ones most often forgotten in a rotation. |
| **Custom Header** | The provider needs more than one header, or a header arrangement the API Key type can't express. |
**OAuth 2.0**
| Type | When to choose it |
| - | - |
| **OAuth 2.0 App** | A user grants access to their own account. This is the only type that produces per-user credentials, so choose it when the agent acts on behalf of the person it's talking to rather than on behalf of your organization. |
| **Client Credentials** | The project itself is the client and no end user is involved. You get token refresh without ever prompting anyone to authorize. |
| **Password Credentials (ROPC)** | The provider offers no alternative. The flow requires your project to hold the resource owner's username and password, which is why most providers now discourage it. Treat it as a compatibility path rather than a design choice. |
**Advanced**
| Type | When to choose it |
| - | - |
| **AWS IAM (SigV4)** | The AWS service signs requests rather than accepting a token. |
| **Azure AD** | The provider authenticates through Microsoft Entra ID. |
| **Kerberos-SPNEGO Auth** | An internal enterprise service authenticates through the domain rather than through a token. |
| **SAML** | The provider expects a SAML assertion rather than a credential on the request. |
| **WS-Security** | A SOAP service expects WS-Security headers. |
**Transport security**
| Type | When to choose it |
| - | - |
| **mTLS** | The provider authenticates the client at the transport layer with a certificate rather than at the application layer with a credential. It pairs with network controls in a way token-based types can't. |
### Fields common to every profile
These fields appear whichever authentication type you choose. They cover how you identify the profile, who supplies the credential, and who can use it.
| Field | Description |
| - | - |
| **Profile Name** | Names the profile for reference from tools and connectors. Choose a name that carries the provider and the environment, because a list of profiles sorted by name is how anyone later works out what to rotate. |
| **Description** | Records what the profile is for. It's the only context another member gets when deciding whether a credential is safe to disable. |
| **Usage Mode** | Decides who supplies the credential and when. It's the most consequential choice on the form, because it also determines the profile's scope and whether users ever see an authorization prompt. |
| **Scope** | Reports the sharing model the platform derived from the usage mode. You don't set it directly, so change the usage mode when you need a different scope. |
| **Visible to project members** | Controls whether project members can use the profile. Turn it off for credentials that only specific automated paths should reach. |
**Usage modes**
| Mode | Behavior |
| - | - |
| **Preconfigured** | An administrator supplies credentials at setup time and users never authorize anything. The resulting scope is shared, so every session uses the same credential and the provider sees one identity for all your traffic. |
| **Preflight** | The user authorizes upfront, before the session starts. The resulting scope is per-user, so the agent acts as the person it's talking to and the provider's own permissions apply per individual. |
**Block until authorized** appears with authorization-based modes. When it's on, the session holds incoming messages and the welcome message until the user finishes authorizing. It trades a slower greeting for the certainty that the agent never starts work it can't complete, which suits agents whose first action always needs the credential.
### Secrets
Every type collects its secrets in a separate section. The platform encrypts them at rest and never displays them again after saving.
That's worth planning around: you can replace a secret but you can't read one back. Record credentials in your own secret store at the time you create the profile, because the platform won't serve as your copy.
### Environment credentials
An existing profile holds a credential set per environment. Environments that carry no credentials of their own fall back to the default set, and the editor states which ones are falling back.
Fallback is a convenience and a risk in the same mechanism. A profile with only default credentials works in production immediately, using the same credential as development. Add environment-specific credentials wherever production must not share an identity, a rate limit, or an audit trail with your lower environments.
### Configuration by authentication type
The configuration and secret fields below are specific to each type. Complete them, then select **Create Profile**.
**API Key**
| Field | Description |
| - | - |
| **Header Name** | Names the header or parameter that carries the key. |
| **Placement** | Decides whether the key travels in a header or a query parameter. Prefer a header where the provider supports it, since query parameters reach server logs and browser history. |
| **Prefix** | Adds a string ahead of the key value for providers that expect one. |
| **API Key** | The key itself. |
**Bearer Token**
| Field | Description |
| - | - |
| **Authorization Prefix** | Sets the scheme name the provider expects ahead of the token. Change it only when the provider documents something other than the standard scheme. |
| **Token** | The token itself. |
**Basic Auth**
| Field | Description |
| - | - |
| **Username** and **Password** | The credential pair the provider accepts. |
**Custom Header**
| Field | Description |
| - | - |
| **Headers** | Holds one or more header name and value pairs, both required on each row. Add a row for each header the provider needs. |
**OAuth 2.0 App**
| Field | Description |
| - | - |
| **Discovery / Server URL** | Points at the provider's base URL. Select **Discover and fill** to populate the endpoints, scopes, and PKCE settings automatically. Try this before completing the endpoint fields by hand, since a discovered configuration matches what the provider actually publishes. |
| **Authorization URL** | The endpoint that presents the consent screen to the user. |
| **Token URL** | The endpoint that exchanges an authorization code for a token. |
| **Refresh URL** | The endpoint that renews an expired token. Without it, users reauthorize whenever the token expires. |
| **Scopes** | Requests the permissions the agent needs. Leave it empty for providers that neither require nor advertise scopes. Requesting more than the agent uses makes the consent screen harder for users to accept. |
| **Require PKCE** | Adds a proof key to the authorization exchange. Enable it wherever the provider supports it, and expect it to be mandatory for providers that follow OAuth 2.1. |
| **PKCE Challenge Method** | Selects the challenge algorithm. |
| **Additional Authorization Parameters** | Appends extra key and value pairs to the authorization URL for providers that need parameters outside the standard set. |
| **Callback URL** | The redirect the provider must send users back to. Copy it into the provider's authorized redirect list before you authorize, because authorization fails at the provider rather than in Studio when it's missing. |
| **Client ID** and **Client Secret** | The application credentials the provider issued. |
**Client Credentials**
| Field | Description |
| - | - |
| **Authorize Using** | Selects how the client proves its identity to the token endpoint. |
| **Token URL** | The endpoint that issues tokens. |
| **Refresh Token URL** | The endpoint that renews them. |
| **Scopes** | Requests permissions, separated by spaces. |
| **Client ID** and **Client Secret** | The application credentials the provider issued. |
**Password Credentials (ROPC)**
| Field | Description |
| - | - |
| **Token URL** | The endpoint that issues tokens. |
| **Scopes** | Requests permissions, separated by spaces. |
| **Audience** | Names the intended recipient of the token for providers that require it. |
| **Client ID**, **Client Secret**, **Username**, **Password** | The application credentials plus the resource owner's own credentials, which this flow needs and the other OAuth types don't. |
**AWS IAM (SigV4)**
| Field | Description |
| - | - |
| **Region** | Sets the region used in the signature. A mismatch here produces a signature error rather than a permission error, which is worth knowing when you're diagnosing a failure. |
| **Service** | Names the AWS service the signature targets. |
| **Bucket** | Names the S3 bucket, for S3 targets only. |
| **Endpoint** | Points at an S3-compatible endpoint when the target isn't AWS itself. |
| **Access Key ID** and **Secret Access Key** | The IAM credentials. |
| **Session Token** | Carries the token for temporary credentials. Profiles built on temporary credentials stop working when those credentials expire, so use long-lived credentials for anything running unattended. |
**Azure AD**
| Field | Description |
| - | - |
| **Azure Tenant ID** | Identifies the directory that issues tokens. |
| **Resource** | Names the resource the token grants access to. |
| **Scopes** | Requests permissions, separated by spaces. |
| **Client ID** and **Client Secret** | The registered application credentials. |
**Kerberos-SPNEGO Auth**
| Field | Description |
| - | - |
| **Service Principal URL** | Identifies the service principal the ticket targets. |
| **Auth Check URL** | The endpoint used to verify that authentication succeeded. |
| **Authentication Type** | Sets the SPNEGO GSS flag the enterprise authentication path uses. |
**SAML**
| Field | Description |
| - | - |
| **IdP Metadata URL** | Points at the identity provider's metadata. |
| **Entity ID** | Identifies this service to the identity provider. |
| **ACS URL** | The endpoint that consumes the assertion. |
| **Private Key** and **Certificate** | The key material used to sign and verify assertions. |
**WS-Security**
| Field | Description |
| - | - |
| **mustUnderstand** | Sets the SOAP mustUnderstand flag on the security header, which tells the recipient it must process the header rather than ignore it. |
| **Password digest algorithm** | Selects the digest algorithm. Move away from the default only when the SOAP provider explicitly requires it, since many older SOAP stacks reject the stronger algorithm. |
| **Username**, **Password**, **Certificate** | The credential the header carries, with an optional certificate. |
**mTLS**
| Field | Description |
| - | - |
| **Client Certificate (PEM)** and **Client Private Key (PEM)** | The certificate and key that identify this client during the TLS handshake. |
| **CA Certificate (PEM)** | Supplies the certificate authority when the provider uses one the platform doesn't already trust. |
### Authorize an existing profile
Profiles that use an authorization-based usage mode need someone to complete the flow before the credential works. Open the profile and select **Authorize**, then select **Save Changes** to keep any edits you made alongside it.
# Configure Microsoft Foundry
Source: https://koreai.mintlify.app/agent-platform/administration/microsoft-foundry
Use models from Microsoft Foundry, including OpenAI, Anthropic, Meta, and Mistral models, through a single provider integration.
Microsoft Foundry hosts models from multiple providers, including OpenAI, Anthropic, Meta, Mistral, and others, through a unified deployment platform. The Microsoft Foundry catalog lets you add these models, and the platform automatically routes inference calls using the appropriate wire format, headers, and authentication scheme.
## Key benefits
* **One provider for multiple models** — Add Microsoft Foundry once and select models from different model families.
* **Automatic format routing** — The platform automatically detects the API format required by the model and routes requests accordingly.
* **Flexible authentication** — Use an API key or an Azure AD Service Principal.
* **Backward compatibility** — Existing connections continue to work without changes.
***
## Supported models
The platform supports any model deployed on Microsoft Foundry. The model catalog lists popular models for easy setup.
If your deployed model is not listed in the catalog, you can add it using the **Custom Model** option. Custom models use the OpenAI Chat format by default. You can override the format if needed.
***
## Prerequisites
Before you configure the provider, ensure you have the following:
1. An **Azure AI Foundry resource** with at least one model deployed.
2. **The resource endpoint**, available in the Azure portal under your Foundry resource's overview. For example, `https://your-resource.openai.azure.com`.
3. **Authentication credentials**, either:
* An **API key** from the Azure portal (**Keys and Endpoint** section), or
* A **Service Principal** with the `Cognitive Services User` role on the resource for automatic token management.
***
## Authentication methods
Microsoft Foundry supports the following authentication methods:
| Authentication method | Description | Recommended for |
| - | - | - |
| **API key** (Manual Credential) | Uses an API key from the Azure portal to authenticate requests. The platform sends the key with each request. | Quick setup, individual use, and development or testing environments |
| **Service Principal** | Enterprise-grade authentication using OAuth 2.0 client credentials. The platform automatically acquires, caches, and refreshes bearer tokens — no manual token rotation required. | Production workloads, team or organization deployments, and environments that require centralized credential management |
For information about configuring each authentication method, see:
* [Add a model with an API key](#add-a-model-with-an-api-key)
* [Add a model with a Service Principal](#add-a-model-with-a-service-principal)
***
## Add a model with an API key
Use an API key to authenticate requests to Microsoft Foundry. The platform sends the key with each request.
* For OpenAI-format models, the key is sent in the `api-key` header.
* For Anthropic-format models, it is sent in the `x-api-key` header.
**Procedure:**
1. Go to **Admin > LLM Providers**.
2. In the **Model Catalog**, select **Add Model**.
3. Select **Microsoft Foundry > Add manually**.
4. Select one of the following options:
* **Choose from popular models** — Select a model from the curated list of Foundry models.
* **Enter deployment details** — Enter the deployment details for a model that is not available in the curated list.
1. **Display Name** — Enter a name for the model.
2. **Model ID** — Enter the deployment or model ID.
3. **Tier** — Select the appropriate model tier.
5. Select **Add to Workspace**.
6. From the model list, locate the Microsoft Foundry model and expand the model row.
7. Under **Connections**, select **Add Key**.
8. In the **Add Connection** dialog, select **Create new credential** and enter the credential details.
| Field | Description |
| - | - |
| **Name** | A display label for this connection. |
| **Provider** | Microsoft Foundry is selected. |
| **API Key** | Enter your Azure API key. |
| **Auth Mode** | Only shown for Anthropic-format models (`API Key` or `Entra Token`). |
| **Endpoint** | Enter the base URL of your Foundry resource, for example, `https://your-resource.openai.azure.com`. Do not include a path suffix. The platform automatically appends the appropriate API path based on the model format. |
| **Deployment ID** | Deployment name in your Azure OpenAI resource. |
| **Anthropic Version** | Only for Anthropic models (for example, `2023-06-01`). |
9. Select **Test Connection** to verify the connection.
10. Select **Save**.
***
## Add a model with a Service Principal
Use a Service Principal to authenticate with Microsoft Foundry using OAuth 2.0. The authentication flow is as follows:
* Register a Service Principal in Azure AD and grant it access to your Foundry resource.
* Create an Azure AD Auth Profile in the platform with the Service Principal credentials.
* When connecting a model, select the auth profile instead of providing an API key.
* At inference time, the platform acquires a short-lived token (`https://cognitiveservices.azure.com/.default` scope), caches it, and refreshes it automatically before expiry.
**Token lifecycle**: Service Principal tokens are acquired on demand, cached, and refreshed before expiry. Tokens are invalidated when the auth profile is updated or deleted. If token acquisition fails, the LLM call returns an authentication error.
**Procedure:**
1. Go to **Admin > LLM Providers**.
2. In the **Model Catalog**, select **Add Model**.
3. Select **Microsoft Foundry**.
4. Select **Use Service Principal**.
5. Select **Create a new profile**.
6. Enter the profile details:
| Field | Required | Description |
| - | - | - |
| Auth Type | Yes | Service Principal (Azure AD). |
| Profile Name | Yes | Enter a name for the profile. |
| Description | No | Enter a description for the profile. |
| Visible to project members | — | Specify whether project members can use this profile. |
| Tenant (Directory) ID | Yes | Enter your Azure AD tenant ID. |
| Subscription ID | Yes | Enter your Azure subscription ID. |
| Application (Client) ID | Yes | Enter the Service Principal application ID. |
| Client Secret | Yes | Enter the Service Principal secret value. |
| Default scope | No | Optionally specify a default scope. |
| Resource Group | No | Select the resource group that contains the Foundry account. |
| Foundry Account | No | Select the Foundry account. |
| Foundry Project | No | Select the Foundry project. |
7. Select **Create profile**.
8. In the **Add Model** dialog, select the Service Principal profile you created.
9. If the profile does not have a default scope, select the **Resource Group**, **Foundry Account**, and **Foundry Project**.
10. Select **List deployments**.
The platform retrieves the model deployments available in the specified Foundry scope.
***
## Endpoint Reference
| Model Format | Full Endpoint (constructed by platform) | API Key Header | Bearer Token Header |
| - | - | - | - |
| OpenAI Chat | `{your-base-url}/openai/v1/chat/completions` | `api-key: ` | `Authorization: Bearer ` |
| Anthropic Messages | `{your-base-url}/anthropic/v1/messages` | `x-api-key: ` | `Authorization: Bearer ` |
***
## Migration from the legacy provider
If you previously configured models using the **Microsoft Foundry (Anthropic)** provider (`microsoft_foundry_anthropic`):
* No action is required. Existing models, connections, and credentials continue working. The legacy provider name is treated as an alias for the unified Microsoft Foundry provider.
* New models should be added using the unified Microsoft Foundry provider, which supports all model families.
* The legacy provider card is no longer shown in the UI for new configurations, but all existing runtime behavior is preserved.
***
## Troubleshooting
| Symptom | Likely cause | Resolution |
| - | - | - |
| "Test Connection" fails with 401 | Invalid API key or expired token | Verify the key in Azure portal; for Service Principal mode, check that the secret hasn't expired. |
| "Test Connection" fails with 404 | Incorrect endpoint or model not deployed | Confirm the base URL and that the model is deployed on the resource. |
| Model not in catalog | Model is not listed in the catalog | Use **Custom Model** and specify the deployment name manually. |
| Auth token acquisition failed | Service Principal credentials invalid or insufficient permissions | Verify Tenant ID, Client ID, and Client Secret; ensure the Service Principal has the **Cognitive Services User** role. |
| Wrong response format or parsing errors | Model format mismatch | Verify the model is correctly identified; use format override if using a custom model. |
***
## Limits and considerations
* **Rate limits** are governed by your Azure AI Foundry resource tier, not by this platform.
* **Token caching** (Service Principal mode) uses preemptive refresh. In rare cases during Azure AD outages, cached tokens may expire without refresh. Calls fail with a clear authentication error and automatically recover when Azure AD is available again.
* **Streaming** works identically for all models; no special configuration is required.
* **Vision and tool use** capabilities depend on the specific model deployed, not the provider configuration. The model catalog shows supported capabilities for each model.
***
## FAQs
| Question | Answer |
| - | - |
| **Do I need to specify which API format my model uses?** | No. The platform detects the API format automatically from the model catalog. For custom models that are not in the catalog, the platform defaults to the OpenAI format. |
| **Can I use the same endpoint for multiple models?** | Yes. If you have multiple models deployed on the same Foundry resource, they share the same base endpoint. Create separate model entries and point their connections to the same URL. |
| **What's the difference between an API key and a Service Principal?** | An API key requires you to provide a key from the Azure portal. A Service Principal uses centrally managed credentials, short-lived tokens, automatic token refresh, and Azure RBAC for fine-grained access control. |
| **Will my existing Anthropic-on-Foundry setup break?** | No. The legacy provider is fully supported as a runtime alias. Existing configurations continue to work without changes. |
# Project settings overview
Source: https://koreai.mintlify.app/agent-platform/administration/project-settings-overview
Project settings give administrators and developers control over who can access the project, which models and tools are available, how the runtime behaves, and what security and compliance policies the system enforces. The **Settings** section covers membership, API keys, model configuration, runtime tuning, data retention, integrations, agent behavior overrides, PII protection, and channel policies.
To access **Settings**, navigate to your project and select **Settings** from the sidebar.
## Before you begin
* Most settings pages require **Admin** access. Developers can view settings but can't modify membership, API keys, or security controls.
* Adding project members requires them to exist as workspace members. Only workspace **Owners** and **Admins** can invite new people into the workspace.
* Model configuration requires at least one LLM provider configured at the workspace level under **Admin** → **Models**.
* Git integration requires a repository URL and authentication (SSH key or personal access token) prepared in advance.
## Access project settings
**Navigation**: **Project** → **Settings**
Membership, API keys, models, runtime config, data retention, variables, and localization.
Git version control and authentication profiles for connectors.
Behavior profiles, alerts, human handoff, agent assist, PII protection, API access, attachments, and omnichannel support.
Response templates, imported modules, publishing, and trace dimensions.
LLM thinking, task prompts, escalation templates, and project deletion.
# Workspace account
Source: https://koreai.mintlify.app/agent-platform/administration/workspace-account
Manage top-level workspace configuration, secrets, billing, and channel integrations.
The **Account** section is your workspace's administrative hub — where you control workspace identity, manage tool credentials, monitor usage, and connect external channels.
* **Navigation**: **Settings** > **Account**
* **Required role**: Owner or Administrator
***
## Workspace settings
Manage your workspace name, URL slug, and configuration from **Settings** > **Account** > **Workspace Settings**.
### General
| Property | Description |
| - | - |
| Workspace Name | The display name for your workspace. |
| URL Slug | Lowercase letters, numbers, and hyphens only. Used in URLs. |
Click **Save Changes** to apply updates.
### Workspace info
The **Workspace Info** section shows the following read-only details about your workspace.
| Property | Description |
| - | - |
| Workspace ID | The unique identifier for this workspace. |
| Status | The current status of the workspace (for example, `Active`). |
| Created | The date the workspace was created. |
### Production approval policy
Set the workspace-wide baseline for how strictly production deployments are gated. Projects inherit this baseline unless they set their own override.
A badge indicates whether the policy is **Overridden at this workspace** or using the platform default, alongside the **Effective** policy currently applied (for example, *Standard - workspace baseline*).
Drag the **Approval strictness** slider to one of three positions, from **Strict** to **Open**. The **Standard** position requires approval, security, and eval sign-off; the publisher may approve their own version.
Click **Save policy** to apply.
### Call recording
**Call Recording** lets you capture voice call audio, store it, and play it back from the session it belongs to. Recording is disabled by default at every level, so no audio is captured until an account admin and a project admin both turn it on.
Call recording is an Enterprise feature. If the Call Recording section does not appear in Workspace Settings, your workspace doesn't have it enabled. Contact your Kore representative to get it enabled. You can't enable it in the platform.
### Danger zone
Deleting a workspace is permanent and removes all projects, members, and data. This action cannot be undone.
Click **Delete Workspace** to permanently delete the workspace. You are prompted to confirm before the action completes.
***
## Secrets
Manage workspace tool secrets, proxy configurations, and OAuth tokens from **Settings > Account > Secrets**.
The Secrets page has three tabs: **Proxy Configs**, **OAuth Tokens**, and **External Vaults**.
### Types of secrets
* Auth Profiles: API keys and credentials used by agent tools to call external services (for example: CRM API key).
* LLM provider credentials: API keys for LLM providers (for example: Anthropic, OpenAI, Azure, Google, Bedrock).
* OAuth tokens: Encrypted access and refresh tokens for OAuth 2.0 connectors (for example: Slack, Google Drive, Salesforce).
* Proxy credentials: Proxy authentication credentials (username, password, certificates) for organizations routing outbound traffic through a proxy.
* External vault references: Pointers to secrets held in your own cloud vault, resolved at execution time.
### Proxy configs
The **Proxy Configs** tab lists HTTP proxy configurations for outbound tool requests, with a running count (for example, *0 configs*) and a refresh icon to reload the list.
**Add a proxy config:**
1. Go to **Settings > Account > Secrets**.
2. Click the **Proxy Configs** tab.
3. Click **Add Config**.
4. In the **Add Proxy Config** dialog, enter:
| Field | Description |
| - | - |
| **Name** *(required)* | A descriptive label for this proxy, for example *Corporate Proxy*, *EU Proxy*. |
| **Proxy URL** *(required)* | The proxy server address, for example `https://proxy.example.com:8080`. |
| **Auth type** *(required)* | The authentication type the proxy expects. Default: **None**. |
| **Environment** *(required)* | The environment this proxy applies to, or **All environments (global)** to apply it everywhere. |
| **URL patterns** *(required)* | Comma-separated patterns for outbound requests that should route through this proxy, for example `*.example.com, api.partner.com`. |
| **Bypass patterns** *(optional)* | Comma-separated patterns to exclude from the proxy, for example `localhost, *.internal`. |
| **Priority** *(required)* | Determines evaluation order when multiple proxy configs could match the same request. Default: `0`. |
| **CA certificate** *(optional)* | PEM-encoded CA certificate content, if the proxy uses a custom or private CA. |
| **mTLS client certificate** *(optional)* | PEM-encoded client certificate, for mutual TLS. |
| **mTLS client private key** *(optional)* | PEM-encoded private key matching the client certificate. |
5. Toggle **Enable immediately** to activate the proxy as soon as it's saved.
6. Optionally, click **Test connection** to check the current proxy URL, authentication, and TLS settings before saving.
7. Click **Add Config**.
### OAuth tokens
The **OAuth Tokens** tab lists all third-party providers authorized via OAuth. Tokens appear here when tools request authorization.
### External vaults
The **External Vaults** tab lets you register a secrets vault in your own cloud directory. Secret values are read at execution time and are never stored by the platform - only a reference to the vault entry is kept.
Currently only **Azure Key Vault** is available. **AWS Secrets Manager** and **GCP Secret Manager** appear in the provider list but are not yet selectable.
**Add an external vault:**
1. Go to **Settings > Account > Secrets**.
2. Click the **External Vaults** tab.
3. Click **Add Vault**.
4. In the **Add Vault** dialog, enter:
| Field | Description |
| - | - |
| **Provider** | The vault provider. Currently **Azure Key Vault**. |
| **Name** | An identifier for this vault. Lowercase letters, digits, and underscores only. Used in `{{secret....}}` references and can't be changed later — choose it deliberately. |
| **Vault URL** | Your vault's URL, for example `https://my-vault.vault.azure.net`. |
| **Azure Tenant ID** | Your Azure AD tenant ID. |
| **Authentication** | How the platform authenticates to your vault. See **Authentication options** below. |
5. Click **Add Vault**.
**Authentication options**
| Option | Description |
| - | - |
| **Platform managed (recommended)** | Kore governs the credential, rotation, and trust settings; these are read-only here. |
| **Service principal** | Use a credential you control. Select an existing **Azure AD auth profile** holding the service principal's client ID and secret - or create one first if none exist. |
***
## Billing and usage
View your subscription plan, manage credits, and configure usage alerts from **Settings > Account > Billing & Usage**.
### Plan and credits
| Card | Description |
| - | - |
| Current Plan | Your active subscription tier. Click **Upgrade Plan** to move to a higher tier. |
| Credit Balance | Credits allocated to your workspace. Displays `No credits allocated` if none are assigned. |
| Top Up Credits | Add credits to your workspace balance. |
For custom deals, dedicated resources, BYOK encryption, and enterprise SLAs, click **Contact Sales**.
### Usage and billing units
Filter billing unit consumption by:
* **Time range**: 7 days, 30 days, or 90 days.
* **All Projects**: Filter by a specific project or view all projects together.
### Alert configuration
Configure alerts to be notified when usage approaches defined thresholds. Existing alerts are listed with their type, threshold, channel, and target.
**Add an alert rule:**
1. Click **Add Alert**.
2. In the **Add Alert Rule** dialog, enter:
* **Alert Type**: The type of alert (for example, Usage Threshold).
* **Threshold (%)**: The usage percentage at which the alert triggers (for example, 80).
* **Channel**: The notification channel (for example, Webhook, Email).
* **Target (URL)**: The webhook URL or email address to notify.
* **Cooldown (minutes)**: Minimum time between repeated alert notifications (for example, 60).
3. Click **Add Alert**.
***
## Connectors
Manage channel connections and SDK channel integrations from **Settings > Account > Connectors**.
The Connectors page has two tabs: **Channel Connections** and **SDK Channels**.
### Channel connections
The **Channel Connections** tab lists all configured channel connections. Click **Add Connection** to set up a new channel.
#### Supported connectors
**Messaging platforms**
| Connector | Auth type | Capabilities |
| - | - | - |
| Slack | OAuth 2.0 | Send/receive messages, manage channels, file uploads. |
| Microsoft Teams | OAuth 2.0 | Send/receive messages, adaptive cards. |
| WhatsApp Business | API key | Send/receive messages, templates, media. |
| Telegram | Bearer token | Send/receive messages, inline keyboards. |
**Communication**
| Connector | Auth type | Capabilities |
| - | - | - |
| Twilio | API key + secret | SMS, voice calls, WhatsApp via Twilio. |
| SendGrid | API key | Transactional and marketing email. |
| Email (SMTP) | Basic auth | Send email via any SMTP server. |
**CRM and business tools**
| Connector | Auth type | Capabilities |
| - | - | - |
| Salesforce | OAuth 2.0 | CRUD operations on objects, SOQL queries. |
| HubSpot | OAuth 2.0 | Contacts, deals, companies, tickets. |
| Zendesk | API key / OAuth 2.0 | Tickets, users, organizations. |
**Webhooks**
| Connector | Auth type | Capabilities |
| - | - | - |
| Inbound webhook | None / HMAC signature | Receive events from external systems. |
| Outbound webhook | API key / Bearer / Custom | Send events to external endpoints. |
**Add a channel connection:**
1. Go to **Settings** > **Account** > **Connectors**.
2. Click the **Channel Connections** tab.
3. Click **Add Connection**.
4. In the **Add Channel Connection** dialog, enter:
* **Connection Name**: A descriptive name for this connection (for example, `Production Slack`).
* **Channel Type**: Select the channel platform: Slack, Microsoft Teams, Email, WhatsApp, or Voice.
5. Enter the credentials for the selected channel type. For Slack:
* **Bot Token**: The Slack bot token (`xoxb-...`).
* **Signing Secret**: The Slack app signing secret.
* **Webhook URL**: The Slack webhook URL (`https://hooks.slack.com/`).
6. Click **Create Connection**.
### SDK channels
The **SDK Channels** tab lists all SDK channels configured for programmatic access to your agents. Each SDK channel provides a unique API key for web widget and mobile app integrations.
| Property | Description |
| - | - |
| Name | The channel name. |
| Environment | The environment this channel is associated with (dev, staging, or production). |
| API Key | The public API key for this SDK channel. Click the copy icon to copy it. |
| Rate Limit | The maximum requests per minute allowed for this channel. Unlimited if not set. |
| Status | Whether the channel is **Enabled** or **Disabled**. |
| Actions | Edit or delete the channel. |
**Add an SDK channel:**
1. Click **Add Channel**.
2. In the **Add SDK Channel** dialog, enter:
* **Channel Name**: A descriptive name (for example, *Web Widget Production*).
* **Project**: The project this channel is associated with.
* **Environment**: The target environment: dev, staging, or production.
* **Enabled**: Toggle on to activate the channel immediately.
* **Rate Limit (RPM)**: Maximum requests per minute. Leave empty for unlimited.
* **Allowed Origins**: Comma-separated list of allowed origins for browser-based integrations (for example, `https://example.com`, `https://app.example.com`).
3. Click **Create Channel**.
# Workspace analytics
Source: https://koreai.mintlify.app/agent-platform/administration/workspace-analytics
Monitor agent performance, sessions, request-level traces, and individual LLM generations across your workspace.
**Workspace Analytics** gives you visibility into how agents behave in production — from aggregate metrics to individual session transcripts and step-level execution logs.
* **Navigation**: Settings > Analytics
* **Required role**: Owner, Admin, or Operator; Member for project-scoped views.
Use the **All Projects** dropdown and time range controls (24h, 7d, 30d, 90d) to filter data across all tabs.
***
## Agents
The **Agents** tab shows aggregate performance metrics for all agents in the selected project and time range.
Agent performance metrics appear once agents start processing sessions.
### Metrics
| Metric | Description |
| - | - |
| Total sessions | Number of agent sessions in the selected time range. |
| Avg. session duration | Mean time from session start to last message. |
| Resolution rate | Percentage of sessions that reached a successful outcome. |
| Avg. messages per session | Mean number of user and agent messages per session. |
| Total tokens consumed | Input and output tokens across all models. |
| Total estimated cost | Calculated LLM cost for the period. |
| Error rate | Percentage of sessions that encountered errors. |
Filter by date range, project, agent, or model. Charts show session volume over time, token usage, cost trends, top agents by session count and cost, and error breakdown.
***
## Sessions
The **Sessions** tab lists individual user sessions across all agents in the selected project and time range.
Session data appears once agents start processing requests.
### Browse sessions
1. Go to **Settings** > **Analytics** > **Session Explorer**.
2. Search by session ID, user message content, or agent name.
3. Filter by status, date range, or agent.
4. Click a session to view the conversation transcript, tool calls, model usage, state transitions, and metadata.
***
## Traces
The **Traces** tab provides a step-by-step execution log of agent runs. It offers two views: **Event Stream** and **SQL Query**.
Trace events appear once agents start processing sessions.
### Event stream
The **Event Stream** view shows a chronological log of all agent events in the selected project and time range.
Each trace records a span tree — a hierarchical view of all operations:
| Event type | What it captures |
| - | - |
| LLM calls | Prompt, response, tokens, and latency. |
| Tool executions | Name, input, output, and duration. |
| Step transitions | Flow step changes for agents with a FLOW section. |
| Guardrail evaluations | Category, score, and decision. |
| Knowledge base queries | Query text, retrieved chunks, and relevance scores. |
Filter events by type:
| Filter | Description |
| - | - |
| All | Show all event types. |
| LLM | LLM call events. |
| Tool | Tool invocation events. |
| Decision | Agent decision and routing events. |
| Error | Events that resulted in an error. |
| Handoff | Agent handoff and delegation events. |
| Agent | Agent lifecycle events. |
| Session | Session start and end events. |
Use the search bar to find events by session ID, agent name, or event type. The total event count is shown next to the search bar.
**Trace search syntax:**
| Operator | Example | Description |
| - | - | - |
| `agent:` | `agent:customer-support` | Filter by agent name. |
| `status:` | `status:error` | Filter by outcome. |
| `model:` | `model:claude-4-sonnet` | Filter by model used. |
| `duration:>` | `duration:>2s` | Filter by total duration. |
| `tokens:>` | `tokens:>5000` | Filter by token count. |
### SQL query
The **SQL Query** view lets you run custom queries against trace event data.
Admin access is required to use the SQL Query view.
| Control | Description |
| - | - |
| Example Queries | A dropdown of pre-built queries to help you get started. |
| SQL editor | Write or paste a SQL query. Use `{tenantId:String}` and `{projectId:String}` for isolation. |
| Clear | Clear the current query from the editor. |
| Execute | Run the query and view results. |
***
## Generations
The **Generations** tab lists individual LLM calls across all agents in the selected project and time range, with token, latency, and cost details for each call.
Generation data appears once agents start making LLM calls.
### Filtering
Generations can be filtered at two levels:
**1. Top-level filters** (apply to the whole list):
| Filter | Behavior |
| - | - |
| Project | Single-select. |
| Environment | Multi-select. |
| Channel | Multi-select. |
| Time range | Presets only: 24h, 7d, 30d, 90d. |
| Search | Full-text search with search-as-you-type. |
**2. Advanced (per-column) filters** - refine the top-level results further. Click **Filters** to add one or more conditions:
1. Click **+ Add filter**.
2. Choose a field - Model, Name, Status, Status Code, Channel Type, Source, Source Subtype, Source ID, and others.
3. Choose an operator (for example: **is**) and a value.
4. Click **Apply Filters** to run the query, or **Clear all** to remove every condition.
**Columns**
| Column | Description |
| - | - |
| Model | The LLM model used for the call. |
| Name | Name of the generation (for example: agent, guardrail evaluation, or action). |
| Status | Outcome of the call (for example: Success). |
| Channel | Channel the request originated from (for example: Web Debug). |
| Source | Origin of the call (for example: Runtime, Runtime Internal, Guardrails). |
| Total Tokens | Combined input and output tokens for the call. |
| Latency | Total time taken for the call to complete. |
| TTFT | Time to first token - latency until the first output token is returned. Populated for streaming calls only; shows "Not applicable" otherwise. |
| Cost | Estimated cost of the call. |
| Start Time | Timestamp when the call started. |
Additional columns - **Session**, **Channel Type**, **Source Subtype**, and **Source ID** - are available but hidden by default.
* Reorder columns by dragging them in the **Customize Columns** sidebar.
* Model is pinned by default.
### Generation payload
Click any row to view the exact request and response, and call metadata. This is the primary place to troubleshoot a specific call - **Event ID** and **Call ID** are the two fields to use when correlating a generation with logs or traces elsewhere.
| Field | Description |
| - | - |
| Event ID | Unique identifier for the trace event. Use to correlate with trace data. |
| Call ID | Unique identifier for the LLM call. Use to correlate with provider-side logs. |
| Session | Session ID the call belongs to. |
| Provider | LLM provider (for example: openai). |
| Input Tokens | Tokens sent in the request. |
| Output Tokens | Tokens returned in the response. |
| Total Tokens | Combined input and output tokens. |
| Latency | Total call duration. |
| TTFT | Time to first token (see Columns above). |
| Status | Outcome of the call. |
The panel has two tabs:
* **Request** - the payload sent to the model.
* **Response** - the JSON response returned by the model, including the generated text and provider metadata.
### Export
Click **Export** to download the current list as a CSV.
Only the entries visible on the current page are exported (up to 50 rows).
# Workspace and team settings
Source: https://koreai.mintlify.app/agent-platform/administration/workspace-and-team
Configure workspace structure, roles, member access, security policies, encryption keys, and environment variables.
**Workspace and Team** settings are your control center for access, security, and runtime configuration. From here you decide who can do what, how the platform protects data, and which values agents use at runtime.
* **Navigation**: **Settings** -> **Team**
* **Required role**: Owner or Admin
***
## Workspace structure
The Agent Platform organizes resources across three levels. You don't need all three. Start with a workspace, add projects as your work grows, and set up an organization only when you need to manage several workspaces together.
| Level | Description |
| - | - |
| **Workspace** | Your team's dedicated environment. It holds all agents, models, connectors, secrets, and members. Each workspace stays fully isolated, so nothing in one workspace reaches another. |
| **Project** | A group of related agents and resources for a specific use case, inside a workspace. A project inherits the workspace's AI model configuration but can override model tier assignments. |
| **Organization** | Optional. It sits above workspaces and carries shared billing and governance across several teams. A single-workspace setup doesn't need one. |
**What the workspace controls:**
* Who has access and what they can do, through team membership and roles.
* Which AI models and providers your team can reach, through LLM policies, token budgets, and rate limits.
* Which features your plan tier unlocks.
**What projects add:**
* Projects draw their members from workspace members and give them project-specific roles.
* You can scope environment variables and secrets per project and per environment, covering development, staging, and production.
***
## Roles and permissions
The platform uses hierarchical role-based access control. Each role inherits every permission the roles below it hold, so you grant the level someone needs rather than assembling one from parts.
### Workspace roles
Workspace roles apply everywhere in the workspace. Assign the narrowest role that still lets someone do their job.
| Role | Description | Key permissions |
| - | - | - |
| Owner | Holds full control. One person owns each workspace. | Transfers ownership, deletes the workspace, manages billing, and performs every admin operation. |
| Admin | Manages team members, models, and workspace settings. | Invites and removes members, configures models, manages secrets and connectors, and reads audit logs. |
| Operator | Manages agent deployments and watches production. | Deploys agents, reads analytics, manages environment variables, and views sessions. |
| Member | Builds and tests agents inside assigned projects. | Creates and edits agents, runs tests, and manages project-level resources. |
| Viewer | Reads workspace resources without changing them. | Views agents, project configurations, and analytics dashboards. |
### Project roles
Project roles apply inside one project. A member can hold different project roles in different projects.
| Role | Description | Key permissions |
| - | - | - |
| Admin | Holds full control over the project's resources and settings. | Manages every project resource, configures settings, and oversees operations. |
| Developer | Creates and modifies agents, tools, workflows, and project assets. | Builds and updates agents, tools, workflows, and assets. |
| Tester | Reads project resources and runs simulations and evaluations. | Reaches project resources, runs tests, and validates agent behavior. |
| Viewer | Reads the project without changing it. | Views project resources and configurations. |
**How workspace and project roles interact:**
* Workspace Owners and Admins hold workspace-wide authority and can administer every project without joining it.
* Other workspace members need explicit project membership before they reach a project.
* Whoever creates a project becomes its Admin and can add other workspace members from **Settings** -> **Members**.
***
## Managing members
Only workspace Owners and Admins manage team membership. Everything in this section needs one of those two roles.
### View current members
Go to **Settings** -> **Team** -> **Members**. The list shows each member's name and email, workspace role, status, and join date. Status covers active, suspended, locked, and deactivated.
### Invite a member
Invitations carry the role you choose, so decide the role before you send one rather than correcting it afterward.
1. Select **Invite member**.
2. Enter the invitee's email address.
3. Select a role: Admin, Operator, Member, or Viewer.
4. Select **Send invite**.
The invitee receives an email holding a unique link that names the workspace and their assigned role. Invitations expire after 7 days, and the platform clears expired ones automatically.
Manage pending invitations from the **Pending Invitations** section on the Members page.
| Action | What it does |
| - | - |
| Resend | Sends a fresh invitation email with a new token and restarts the 7-day expiry. |
| Revoke | Cancels the invitation so nobody can use the link. |
If a member reports that the invitation never arrived, find it in the pending list and resend. You don't need to revoke it first.
### Change a member's role
A role change takes effect immediately. The member's permissions update on their next page load or API call.
1. Find the member in the Members list.
2. Select the role dropdown next to their name.
3. Select the new role and confirm.
**Rules the platform enforces:**
* You can't assign a role above your own. An Admin can't promote anyone to Owner.
* Only the workspace Owner promotes a member to Admin.
* Only the workspace Owner transfers ownership.
* You can't demote yourself. Ask another Owner or Admin to change your role.
### Remove a member
Removing a member cuts their access at once, so confirm the person no longer needs the workspace before you start.
1. Find the member in the Members list.
2. Select the three-dot menu next to their row.
3. Select **Remove member** and confirm.
**What happens when you remove a member:**
* They lose access to every workspace resource immediately.
* The platform drops their project memberships inside this workspace.
* Resources they created, such as agents and knowledge bases, stay in the workspace.
* Sessions they started keep running, but they can't start new ones.
Removing a member doesn't delete their Agent Platform account. They keep access to any other workspace they belong to.
### Transfer workspace ownership
Only the current workspace Owner transfers ownership, and the transfer changes both people's roles at once.
1. Go to **Settings** -> **Team** -> **Members**.
2. Select the three-dot menu next to the target member.
3. Select **Transfer ownership**, then confirm by typing the workspace name.
After the transfer, the previous Owner becomes an Admin and the new Owner gains full control, including the ability to delete the workspace.
***
## Role management
Role Management decides what people can see and do across your workspace and projects. Roles come in two types.
* **Custom roles**, which you define yourself.
* **System roles**, which the platform provides and which cover the most common access levels.
Find Role Management under **Settings** -> **Role Management**.
***
## Custom roles
Custom roles let you build access levels that match your team, such as a role that only reads configuration or one that releases to a limited set of environments.
The **Custom roles** tab lists every custom role in your workspace, showing its name and description, how many permissions it carries, when someone created it, and quick actions to edit or delete it.
### Creating a custom role
Select **+ Create Role** to open the creation panel, then complete these fields.
* **Role name**, a short identifiable name.
* **Description**, optional context for what the role does. It earns its place once you hold several similar roles.
* **Parent role**, optional. Base the role on an existing custom role to inherit its permissions, or choose **No parent role** to start from nothing. The panel shows the resulting parent chain, so you can see what you're inheriting before you save.
### Assigning permissions
The panel groups permissions by category. Select the actions this role may perform. A running count at the bottom tracks how many you've selected.
The picker gives identifiers without describing them. See the [Permissions reference](/agent-platform/permissions-reference) for what each permission grants, which ones reverse a protection, and which ones to keep apart from one another.
The picker shows permissions outside your own role's grant scope but won't let you select them. You can't grant access you don't already hold.
### Scoping release environments
Release scoping decides where a role may push configurations, which matters most for production.
When the role includes the Workbench release permission, you can also restrict where it releases configurations, across **Development**, **Staging**, and **Production**. The scoping covers release actions only. It grants no production access on its own, and you must still select the underlying release permission separately.
Select **Create** to save the role, or **Cancel** to discard it.
***
## System roles
The **System roles** tab lists the built-in roles, grouped by where they apply. You can't edit or delete them, but you can expand any role to see the grants it carries.
Select **View grants** next to a role to read its full permission list.
***
## When to use custom or system roles
Choose a **system role** whenever a built-in option already matches what someone needs. System roles need no setup and stay current on their own. Build a **custom role** when you need finer control, such as read-only access to a subset of permissions, a role scoped to specific release environments, or a role that extends another custom role through a parent chain.
***
## Security and compliance
Go to **Team** -> **Security & Compliance** to manage authentication and access controls. The page carries two tabs: **SSO** and **Audit Logs**.
**Required role**: Owner or Admin.
### Single sign-on
SSO lets your users authenticate through an external identity provider.
**Plan requirement**: Enterprise plan only.
Only organization owners or admins can manage SSO for a workspace. Other roles see the SSO tab as read-only, with a **Read-only** badge and an **Org admin access required** banner.
**SSO Protocol**
Under **SSO Protocol**, select the protocol your identity provider uses:
| Protocol | Description |
| - | - |
| **SAML 2.0** | For enterprise identity providers like Okta, Azure AD, and OneLogin. |
| **OpenID Connect** | For OIDC-compatible identity providers. |
Then select an **Identity provider** from the dropdown. Choosing **Custom SAML 2.0** lets you enter the IdP SSO URL, entity ID, and certificate details manually.
**Values to copy into your identity provider**
After you select a protocol and identity provider, the **Values to copy into your identity provider** box shows the endpoints the platform generates. You create and edit nothing here. In your identity provider, set the service-provider audience or entity ID to the SP Entity ID, and set the assertion consumer service (ACS) endpoint to the ACS URL. These are distinct values.
| Field | When to use it |
| - | - |
| **SP Entity ID / Audience URI** | Enter it as the **Entity ID**, **Identifier**, or **Audience URI** in your identity provider. It identifies the Agent Platform service provider and lets the identity provider confirm that a SAML assertion targets this application. |
| **ACS URL (SP- and IdP-initiated)** | Enter it as the **Assertion Consumer Service URL** or **Reply URL** in your identity provider. The same URL serves both service-provider-initiated and identity-provider-initiated sign-ins. |
| **SAML metadata URL** | Give it to your identity provider so it imports the service provider metadata automatically, covering the entity ID, ACS URLs, supported bindings, and signing certificates. |
Each field has a copy button. Select **Refresh** at the top of the page to reload the current values. Refreshing neither regenerates nor changes them.
**SAML Configuration**
Enter your identity provider's SAML details. For **Custom SAML 2.0**, you enter these values manually.
| Field | Description |
| - | - |
| **SSO Entry Point URL** | Your identity provider's SAML SSO URL, for example `https://idp.example.com/sso/saml`. |
| **Issuer / Entity ID** | Your identity provider's issuer or entity ID. |
| **X.509 certificate 1** | The signing certificate your identity provider uses. Paste it in PEM format, including the `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines. |
The platform accepts up to 2 signing certificates. Use a second certificate to rotate certificates without downtime.
* To keep the saved certificate, leave the certificate field blank.
* To replace the saved certificate, enter the new certificate in the first field.
* To rotate, select **Add certificate** to append a second certificate without removing the current one.
After you enter the details, use the buttons below the form:
| Button | Description |
| - | - |
| **Save Configuration** | Stores the values. |
| **Test configuration** | Verifies the connection with your identity provider. |
| **Activate SSO** | Turns SSO on for the workspace. |
| **Pause SSO** | Turns SSO off without deleting the configuration. |
**Set up OIDC SSO**
Go to **Team** -> **Security & Compliance** -> **SSO**, select **OpenID Connect**, enter the configuration details, and select **Save Configuration**.
**SSO Access Policy**
Control workspace access behavior after successful SSO authentication. These settings save separately from your provider credentials.
| Setting | Description | Default |
| - | - | - |
| **Force SSO** | Require all users to sign in via SSO. | Disabled |
| **Auto-accept pending invites** | Automatically accepts a single pending invitation for a user who signs in via SSO but has no workspace membership yet. | Disabled |
Once you enable **Force SSO**:
* Verified domain users lose password-based login.
* Users outside the verified domain carry on as before.
* Workspace Owners keep password-based login as a fallback.
Select **Save Access Policy** to apply changes.
**Recovery Exclusions**
Use recovery exclusions to give specific users emergency access that bypasses SSO or MFA. Each exclusion needs a documented reason and an evidence reference, so the bypass leaves an audit trail.
The **Recovery Exclusions** list shows each active exclusion with its bypass type and expiry, for example **SSO bypass · Never expires**. Select the **X** next to an exclusion to revoke it.
To grant an exclusion, complete the **Grant an exclusion** fields, then select **Grant exclusion**.
| Field | Description |
| - | - |
| **User ID** | The ID of the user who receives the exclusion. |
| **Normalized email** | The user's email address in normalized form. |
| **Bypass type** | The requirement to bypass, such as **SSO bypass**. |
| **Reason** | Why you're granting the exclusion. Include a ticket or evidence reference, for example `JIRA-123`. |
| **Expires at** | The date and time the exclusion ends. |
| **Never expires** | Keeps the exclusion active until you revoke it manually. When you turn it on, **Expires at** no longer applies. |
Grant exclusions only for emergency access, and set an expiry unless the user needs a permanent exception.
**JIT Provisioning**
Just-in-time (JIT) provisioning automatically creates a workspace membership when a user authenticates through SSO for the first time.
| Setting | Description |
| - | - |
| **Enable SSO JIT provisioning** | Lets eligible SSO users receive a workspace membership automatically. |
| **Enable IdP group-to-role mapping** | Assigns workspace roles from your identity provider's group or attribute claims. It requires JIT provisioning to be enabled with a provisioning mode other than **Disabled**, and you must activate the workspace SSO provider first. Mapping applies only to eligible SSO provisioning, and existing manual memberships stay unchanged. |
| **Provisioning mode** | Controls how new SSO users join the workspace. See the modes below. |
| **Default role for new SSO members** | The role a user receives when the platform first creates their membership. Choose **Member** or **Viewer**. The platform also applies this role when the user's IdP claims match no mapping rule. A matching mapping rule can change an existing SSO-managed role. Manual and invitation-managed roles stay unchanged. |
| **Allowed email domains** | Limits automatic provisioning to users with these email domains. |
| Provisioning mode | Behavior |
| - | - |
| **Disabled - users must be invited** | The platform doesn't auto-provision users. Users must receive an invitation. |
| **Enable - auto-provision with default role** | The platform creates a membership with the default role when a user first signs in through SSO. |
| **Invite-only - require pending invite** | A user can join only if they have a pending invitation. |
Allowed email domains apply only to new automatic SSO provisioning. They don't change existing members or workspace invitations. The workspace owner's domain is always included and can't be removed. You can add up to 50 domains.
To add a domain, enter it under **Add another allowed domain**, then select **Add domain**. Enter the domain only, such as `example.com`, not an email address.
Select **Save** to apply your JIT provisioning changes.
**Group-to-Role Mapping**
Group-to-role mapping assigns a workspace role based on a claim or SAML attribute that your identity provider sends. Configure and activate an SSO provider before you add rules.
To add a rule, complete the following fields, then select **Add rule**.
| Field | Description |
| - | - |
| **IdP claim / SAML attribute** | The claim or attribute name your provider sends, for example `groups`. Enter it exactly as it appears in the provider's assertion or token. |
| **Claim value** | The value to match. |
| **Match condition** | How the platform compares the claim to the value, such as **Member of**. |
| **If matched** | The action to take when the rule matches, such as **Grant role**. |
| **Workspace role** | The workspace role to grant. |
| **Rule priority** | The order in which rules run. A lower number runs first. Default: 100. |
How the platform applies rules:
* If the user's claims match no rule, a new eligible member receives the JIT default role.
* If you have active rules but the user's sign-in carries no claims, the platform creates no membership and uses the existing workspace access-required flow.
* A matching rule can change the role of an existing SSO-managed member. It doesn't change roles that were assigned manually or through an invitation.
***
## Audit logs
Audit logs give you a tenant-scoped, filterable record of every significant workspace action. Use them to investigate incidents, confirm compliance, and review team activity.
* **Navigation**: **Settings** > **Team** > **Audit Logs**
* **Required role**: Owner or Admin
* **Export**: Professional and Enterprise plans
Select **Refresh** to reload the log, and **Export CSV** to download the view you're currently filtering. Use the filter bar to narrow results by search text, date range, or category.
#### Filtering
* **Search** matches actor, action, resource, trace, IP, or metadata.
* **From / To** set the date range. Quick presets - **1h**, **24h**, **7d**, **30d** - apply a range in one click, and **Reset** clears every active filter.
* **Categories** appear as toggle buttons; select one or more to narrow the log to those event types.
* **More filters** exposes additional filter fields beyond search, date, and category.
* Select **Apply** to run the filters.
#### Summary metrics
These five figures sit above the log and describe the view your filters produce.
| Metric | Description |
| - | - |
| Matching events | Counts every event your current filters match. |
| Failed on page | Counts the failed events on this page. |
| Actors on page | Counts the distinct actors on this page. |
| Active filters | Counts the filters you've applied. |
| Last event | Times the most recent event. |
#### Event categories
Categories group actions by the part of the platform they touch, which is how you narrow a broad search quickly. The platform organizes events into the following categories: Auth & access, Workspace governance, Project, agent & workflow configuration, Deployment lifecycle, Evaluation, Channel configuration, Tools, modules & credentials, Data protection, Operate sessions, Insights, dashboards & reports, KMS, Connector configuration, and Archives, retention & Git.
#### Log table columns
Each row records one action, and these columns tell you who did what, where, and when.
| Column | Description |
| - | - |
| Timestamp | Times the action. |
| Category | Names the event category. |
| Action | Names the specific action, such as `agent.dsl_updated`. |
| Actor | Identifies the user or system that acted. |
| Target | Names the resource the action affected. |
| Project | Names the associated project. |
| Source | Names the origin, either Studio or runtime-store. |
| IP | Records the request IP address. |
| Trace | Records the trace ID when one exists. |
#### Viewing event details
Select any row to open a detail panel for that event. Alongside the columns shown in the table, the panel adds:
| Field | Description |
| - | - |
| Event type | The full event name, matching the Action column (for example, `project_member_role_changed`). |
| Actor type | Identifies whether the actor was a `user` or a system process. |
| Resource type | Names the type of resource the target belongs to (for example, `project_member`). |
| Environment | Names the environment the event occurred in (for example, `dev`). |
| Metadata | A JSON object with event-specific detail - for a role change, for example, the target user ID and the previous and new role. |
#### Retention
Retention decides how far back an investigation can reach, so check it before you rely on the log for compliance evidence.
* Audit logs follow the workspace retention policy.
* Professional and Enterprise plans keep at least 90 days.
* Contact support to extend retention.
***
## Key management
The Key Management Service lets you control the encryption keys that protect sensitive workspace data. Rather than relying on platform-managed keys, you can supply your own key material from a supported cloud provider.
* **Navigation**: **Settings** -> **Team** -> **Key Management**
* **Required role**: Owner
* **Plan requirement**: Enterprise plan only
### Configuration
Select a **Provider** to configure the tenant-level KMS. The provider you choose decides which fields the page shows.
**Supported KMS providers**
| Provider | Key type | Region support |
| - | - | - |
| **Platform Managed** | Platform-governed keys | Managed by platform |
| **AWS KMS** | Symmetric CMK (AES-256) | All AWS regions |
| **Azure Key Vault** | RSA or AES keys | All Azure regions |
| **Azure Managed HSM** | Symmetric AES-256 keys | All Azure regions |
| **Google Cloud KMS** | Symmetric AES-256 keys | All GCP regions |
| **External KMS** | Any REST-compatible KMS | Self-hosted or SaaS |
#### Platform Managed
Use the platform-managed KMS provider. This mode stores no tenant-specific external key connection.
Kore governs Platform Managed encryption. You can select it, but its policy, compliance, rotation, and re-encryption settings stay read-only. To bring your own key, choose a different provider.
#### AWS KMS
AWS KMS signs and wraps with a customer managed key you already hold in AWS. The auth method you choose decides whether the platform reaches it using static AWS credentials or by assuming a role in your AWS account.
| Field | Description |
| - | - |
| AWS KMS Endpoint | Holds the URL of the AWS KMS. |
| AWS Region | Names the AWS region that hosts the key. |
| KMS Key ARN | Holds the full ARN of the CMK, such as `arn:aws:kms:us-east-1:123456789:key/abcd-1234`. |
| Auth Method | Chooses **Service Account / Service Principal** or **AssumeRole**. |
**Service Account / Service Principal**
Store encrypted AWS access keys for this tenant. Use temporary credentials only with a session token.
| Field | Description |
| - | - |
| Access Key ID | Required. The AWS access key ID for the credentials the platform stores. |
| Secret Access Key | Required. The AWS secret access key paired with the Access Key ID. |
| Session Token | Optional. Use for temporary STS credentials. |
**AssumeRole**
AssumeRole lets the platform obtain short-lived AWS credentials by assuming an IAM role in your account, rather than storing long-lived access keys. Store the customer IAM role ARN. The platform uses the tenant external ID when assuming this role.
| Field | Description |
| - | - |
| Customer IAM role ARN | Required. Holds the ARN of the IAM role the platform assumes, such as `arn:aws:iam::123456789012:role/tenant-kms-role`. |
**AWS trust policy setup**
Generate the trust policy values the AWS account administrator must configure before you save or test AssumeRole. Enter the customer IAM role ARN, then select **Generate setup policy**.
Select **Test Connection** to verify the provider connection before you save the configuration.
#### Azure Key Vault
Azure Key Vault holds the key, and the auth method you choose decides whether the platform reaches it through your own service principal or its own managed application.
| Field | Description |
| - | - |
| Key Vault URL | Points at the vault. |
| Azure key version (optional) | Pins this scope to one Azure key version using the 32-character hexadecimal version ID. Leave it empty to follow Azure's current key version. |
| Key Name | Names the key inside the vault. |
| Auth Method | Chooses **Service Account / Service Principal** or **Platform workload identity**. |
**Service Account / Service Principal**
Use the Azure app registration or service principal that can read, wrap, and unwrap this key.
For cross-tenant access, register this application in the customer Azure tenant through admin consent, then assign the Key Vault Crypto User role on the target key vault.
| Field | Description |
| - | - |
| Tenant ID | Identifies the Azure directory. |
| Client ID | Identifies the application. |
| Credential type | Chooses **Client secret** or **Certificate (PEM)**. |
| Client Secret | Required when you choose Client secret. |
**Platform workload identity**
Enter the customer Entra directory ID. The platform uses its own managed Azure application, so you supply no client ID, secret, or federated credential.
| Field | Description |
| - | - |
| Customer Entra directory ID | Identifies the customer's Azure directory. |
Your Azure administrator must grant the platform's managed Azure application access to the target key vault.
Select **Test Connection** to verify the provider connection before you save the configuration.
#### Azure Managed HSM
Azure Managed HSM stores the key in dedicated hardware, which suits compliance regimes that won't accept a software-backed vault.
| Field | Description |
| - | - |
| **Managed HSM URL** | Points at your HSM instance endpoint. |
| **Azure key version (optional)** | Pins the scope to a specific Azure key version using the 32-character hexadecimal version ID. Leave it empty to follow Azure's current key version. |
| **Managed HSM Key Name** | Names the key to use. |
| **Auth Method** | Chooses **Service Account / Service Principal** or **Platform workload identity**. |
**Service Account / Service Principal**
Use the Azure app registration or service principal that can read, wrap, and unwrap this key.
For cross-tenant access, register this application in the customer Azure tenant through admin consent, then assign the Key Vault Crypto User role on the target key vault.
| Field | Description |
| - | - |
| Tenant ID | Identifies the Azure directory. |
| Client ID | Identifies the application. |
| Credential type | Chooses **Client secret** or **Certificate (PEM)**. |
| Client Secret | Required when you choose Client secret. |
**Platform workload identity**
Enter the customer Entra directory ID. The platform uses its own managed Azure application, so you supply no client ID, secret, or federated credential.
| Field | Description |
| - | - |
| Customer Entra directory ID | Identifies the customer's Azure directory. |
#### Google Cloud KMS
Google Cloud KMS holds the key, and the platform addresses it through its full resource path rather than a name.
| Field | Description |
| - | - |
| Location | Names the GCP location. |
| Key Resource Name | Holds the full resource path, such as `projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/my-key`. |
| Auth Method | Chooses **Default Credentials** or **Service Account / Service Principal**. |
| Credential Overrides (JSON) | Optional. Overrides the default credentials with a JSON object. |
#### External KMS
Connect any REST-compatible KMS, whether you host it yourself or consume it as a service.
| Field | Description |
| - | - |
| External KMS Endpoint | Holds the base URL of the external KMS. |
| External Key ID | Identifies the key at the external provider. |
| Auth Method | Chooses **API Key**, **mTLS**, **OAuth2 Client Credentials**, or **HMAC-SHA256**. |
| Credential Overrides (JSON) | Optional. Holds a JSON object carrying provider-specific credential fields. |
**External endpoint validation**
Before you save the tenant KMS configuration, enter the endpoint, key ID, and auth method, then select **Validate Endpoint** to confirm that the platform reaches and authenticates with the external provider.
### Policies
These two settings decide how the platform behaves when the KMS becomes unreachable and which compliance profile it applies.
| Setting | Options | Default |
| - | - | - |
| **Failure Policy** | **Fail Closed**, which stops all encryption and decryption. **Graceful Degradation**, which continues with cached DEKs until the KMS reconnects. | Fail Closed |
| **Compliance Level** | **Standard**, **PCI DSS**, **HIPAA**, **GDPR**, **SOC 2**, **NIST SP 800-57**, **FIPS 140-3** | Standard |
### Rotation and re-encryption settings
Keys rotate automatically when your provider supports scheduled rotation, or manually when you create a new key version on demand from the Encryption Keys tab.
Tune how often the platform generates DEKs, how long a KEK may live, and whether retired DEKs are ever destroyed.
| Setting | Description |
| - | - |
| DEK Epoch Interval (hours) | Sets how often the platform generates a DEK, in hours. |
| DEK Max Usage Count | Caps how many times a DEK is used before it rotates. |
| KEK rotation policy (days) | Sets how often the KEK rotates, in days. |
| Last KEK rewrap completed | Read-only. Times the most recent KEK rewrap, or reads `Never` when none has run. |
Re-encryption also carries **Concurrency**, which sets parallel jobs, **Batch Size**, which sets records per batch, and **Max Retries**, which sets how many times a failed job retries. Select **Save Configuration** to apply your changes.
### Scopes
Scopes let you run different KMS providers for tenant environments, projects, and project environments, so one project can hold its own key without changing the rest.
***
## Environment variables
Environment variables are key-value pairs that inject configuration into your agents and tools, so you don't hardcode values in ABL definitions.
Use them for endpoints, feature flags, timeouts, and anything else that differs between environments. The platform resolves them at runtime and scopes them by environment and project.
A variable takes its identity from its **key** within a project. It can carry a **Global** value that every environment shares, plus optional values for **Development**, **Staging**, and **Production**. When a key holds both a global value and an environment-specific one, the environment-specific value wins.
Environment variables behave as follows:
* **Environment-specific**. One key carries different values in development, staging, and production.
* **Encrypted at rest**. The platform encrypts stored values using AES-256-GCM.
* **Resolved at runtime**. The agent resolves values when it runs.
* **Immediately effective**. Changes reach new agent sessions without a redeploy.
- Environment variables belong to a project.
- Manage variables centrally from **Workspace Settings** -> **Env Variables**, or from the project's **[Deployments -> Environments](/agent-platform/deployment)** page.
### Add environment variables
Adding a variable takes five steps, and the namespace you choose at step 4 is the one decision you can't change afterward without recreating the variable.
1. Go to **Workspace Settings** -> **Env Variables**.
2. Select the project.
3. Select the target environment, or create a **global** variable that covers every environment.
4. Select **Add Variable**. The platform adds the variable to the **Default** namespace. To use a different namespace, select it from the **Namespace** dropdown before you create the variable.
5. Enter the following details, then select **Save**.
| Field | Description |
| - | - |
| Key | Names the variable uniquely within the selected project and environment. The key must start with a letter, contain only letters, numbers, and underscores, and stay unique inside the project and environment. |
| Value | Holds the value the variable carries for the selected environment. |
| Description | Tells project collaborators what the variable is for. |
| Secret | Masks the value after you save it. Turn it on for anything sensitive. |
### Reference environment variables
Reference a variable using `{{env.KEY}}` syntax. The agent resolves it at runtime based on the active deployment environment.
You can reference environment variables in these constructs:
* HTTP tool endpoint, headers, auth, and request body
* MCP tool call arguments
* RESPOND text and rich content, covering quick replies, cards, forms, and KPI or chart widgets
* ESCALATE and handoff messages, GATHER re-prompts, and guardrail block or redirect messages
* Agent persona, goal, and limitations, meaning the system prompt
**Example: HTTP tool endpoints**
```xml theme={null}
TOOLS:
lookup_customer(customer_id: string) -> {name: string, tier: string}
description: "Look up customer details"
type: http
endpoint: "{{env.API_URL}}/customers/{customer_id}"
method: GET
auth: bearer
timeout: "{{env.TIMEOUT}}"
```
**Example: response templates**
```xml theme={null}
COMPLETE:
- WHEN: ticket_created IS SET
RESPOND: |
Your ticket has been created.
Expected response time: {{env.SLA_HOURS}} hours.
Support portal: {{env.SUPPORT_PORTAL_URL}}
```
**Example: memory**
```xml theme={null}
MEMORY:
PERSISTENT:
- PATH: user.preferences
TTL: "{{env.MEMORY_TTL_DAYS}}d"
```
### Edit a variable
Select the **Edit** icon next to a variable to change its value, description, or secret setting. Your change reaches new agent sessions immediately.
Sessions already running keep the values they resolved when they started, and only new sessions pick up your change. HTTP tools and MCP tools are the exception and use the updated values as soon as you save.
### Delete a variable
Select the **Delete** icon and confirm.
Before you delete a variable, confirm that nothing references it. Anything that references a deleted variable hits a runtime resolution error.
### Import or export environment variables
A bulk import creates or updates many variables at once, which saves you entering them one by one when you set up a new environment.
1. Select **Import**.
2. Select the target environment.
3. Paste the keys in JSON format, as shown below.
```xml theme={null}
[
{"key": "API_KEY", "value": "sk-xxx", "isSecret": true},
{"key": "DB_URL", "value": "postgres://..."}
]
```
4. Review the import preview.
5. Select **Import**.
You can also export environment variables as a `.json` or `.env` file.
### Variable namespaces
Namespaces organize environment variables into groups, which keeps a large set navigable. Every project holds a **default** namespace, and you can create up to 25 namespaces per project.
#### Create a namespace
1. Go to **Settings** -> **Environment Variables**.
2. Select **Namespaces**.
3. Enter a namespace name, display name, description, and color.
4. Select **Create**.
# Agent Tables
Source: https://koreai.mintlify.app/agent-platform/agent-tables
Agent Tables provide a built-in structured data store for your agents. You can create tables in Studio to store operational data such as customer records, appointments, orders, inventory, or support tickets. Agents can access this data at runtime through **Table tools** without requiring an external database.
Unlike conversational memory or knowledge sources, Agent Tables store structured, persistent business data that agents can create, query, update, and delete, depending on the operations permitted to the agents.
The **Tables** page appears under **Resources** only if the feature is enabled for your workspace. If you don't see it, [contact Support](https://support.kore.ai/).
## Step 1: Create a table
* Navigate to **Resources > Tables.**
* Click **New Table.**
* Enter table details.
* Display name: User-friendly table name displayed in Studio.
* Table name (slug): Unique identifier used internally and by runtime. This must be used in the queries.
* Scope: Determines who can access the table's data.
* Description: Briefly describe the usage of the table.
| **Scope** | **Description** | **Typical use cases** |
| - | - | - |
| **Project** | Shares data across all sessions and end users within the project. Suitable for reference or operational data that should be available to every conversation. Agent writes are audited as declassification events. | Product catalogs, inventory, FAQs, departments, pricing information |
| **End User** | Creates a private data partition for each customer. Each end user can access only their own records. Data is retained across conversations until deleted or erased. | Customer profiles, saved addresses, preferences, loyalty points, order history |
| **Session** | Creates a private data partition for a single conversation. Data is automatically cleared when the session ends. | Shopping carts, in-progress forms, temporary selections, conversation state, scratchpad data |
* Next, define the table schema by specifying the columns and their properties.
* **Name** - Specifies the unique name used to identify the column in the table.
* **Type** - Defines the data type that the column can store, such as String, Number, or DateTime. To add a reference key to a column in another table, use Reference key. Set the target table and column, and choose what happens when the referenced row is deleted: Restrict (block the delete), Set null, or Cascade (delete the referencing rows). A Reference column can't be sensitive.
* **Required** - Makes the column mandatory when creating or updating a row.
* **Default value** - Automatically assigns a value if none is provided during row creation. The options are **None**, **Auto ID** (a generated unique ID, for String columns), and **Now** (the current server time, for Datetime columns).
* **Indexed** - Creates an index on the column to improve query performance for filtering and sorting.
* **Unique** - Ensures that each row contains a distinct value for the column.
* **Sensitive** - Encrypts the column data at rest and redacts its values in queries and logs to protect sensitive information.
* Save the table.
You can add columns to an existing table later. Changes that drop a column or change its type are blocked.
## Step 2: Add records
After creating a table, add records to populate it with data that your agents can retrieve and update during conversations.
* Click **Add record**.
* Enter values for each column. Required fields must be completed before saving the record.
* Click **Save** to add the record to the table.
Repeat these steps to add additional records as needed.
**Points to note**
* Values entered must match the data type defined for each column.
* Columns marked as **Required** must contain a value.
* Open the table details to review the records added to the table, view the table schema, or run sample queries to test.
## Step 3: Create a tool for the table
After creating a table and adding records, create a **Table tool** so your agents can access and manipulate the table data during conversations.
* Go to **Tools > Tables**.
* Create a new table tool and configure the following properties.
| Field | Description |
| - | - |
| Tool name | Enter a unique name for the tool. This name is used to identify the tool in your agent. |
| Description | Provide an optional description of the tool's purpose. |
| Table | Select the Agent Table that the tool will read from or write to. |
| Joinable tables | Select additional tables that this tool can join in SQL queries. Joins are supported only on declared foreign-key columns. Leave this empty if the tool accesses a single table. |
| Scope | Displays the scope of the selected table (Project, End User, or Session). This should match the scope defined for the table. |
| Operations | Choose the operations that the tool can perform on the table. Follow the principle of least privilege. Enable only the operations required by your agent. For example, if the agent only retrieves information, select only **get**, **query**, and **count**. |
| SQL statement | You can provide an SQL template to define how the tool interacts with the selected table. The statement can include parameter placeholders (for example, `:status`) that are populated from the tool's input parameters at runtime. Example: `SELECT * FROM customer_orders WHERE status = :status`. Note that this query is used to limit the results presented to the agent. |
| Input parameters | The values the agent supplies when it calls the tool. Each `:name` placeholder in the SQL statement needs a matching parameter. Click **Parse** to create the parameters from the statement automatically. Supported types are string, number, integer, and boolean. Add a description to each parameter so the agent knows what to pass. |
| Variable Namespaces | Select all the namespaces that the tool can access. |
* After completing the configuration, click **Create Tool**. The Table tool is added to your project and can be attached to one or more agents.
* To control which **variable namespaces** the tool can access, open the tool after you create it and select the namespaces on its detail page. This is optional.
### Use a SQL statement
The SQL statement is optional. Use it when you want the tool to do one specific, predictable thing instead of letting the agent build its own queries.
When you add a statement, the agent can't change what the statement does. It supplies only the values for the `:name` placeholders. This lets you control exactly which columns, filters, and sort order the agent can use.
Examples:
**1. Look up orders by status \[query]:**
```sql theme={null}
SELECT order_id, status, total
FROM customer_orders
WHERE status = :status
ORDER BY order_id
LIMIT 10
```
**2. Look up one order \[query]:**
```sql theme={null}
SELECT *
FROM customer_orders
WHERE order_id = :order_id
```
**3. Add a row \[insert]:**
```sql theme={null}
INSERT INTO customer_orders (order_id, status)
VALUES (:order_id, :status)
```
**4. Update a row \[update]:**
```sql theme={null}
UPDATE customer_orders
SET status = :status
WHERE order_id = :order_id
```
**5. Delete a row \[delete]:**
```sql theme={null}
DELETE FROM customer_orders
WHERE order_id = :order_id
```
**Rules**
* The statement must be a single statement that starts with **SELECT**, **INSERT**, **UPDATE**, or **DELETE**. The matching operation must be enabled on the tool. For example, a **SELECT** statement needs **query**, and an **UPDATE** statement needs **update**.
* Write placeholders as `:name`, for example, `:status`. Never paste user-provided values into the statement.
* The statement must contain at least one placeholder. A statement with no `:name` placeholder returns an error. If a tool needs no inputs, don't use a SQL statement.
* Every placeholder must receive a value when the tool is called. If a value is missing, the call fails with the error: **Missing query template parameter "name"**. Define each placeholder as an input parameter, and mark it as required so the agent always provides it.
* A statement can filter and sort only on indexed columns.
* Supported: **WHERE**, **ORDER BY**, **LIMIT**, **JOIN** (on declared foreign keys), and column lists.
* Not supported: **GROUP BY**, **HAVING**, **OFFSET**, **UNION**, **WITH**, subqueries, and aggregate functions such as **COUNT** or **SUM**. To count rows, enable the **count** operation.
* A read returns at most **50 records**. If the response has more records, the result includes a cursor that the agent can use to fetch the next set of records.
* If a call returns **"Query templates are not enabled for this project"**, SQL statements are turned off for your workspace. Contact your administrator.
## Step 4: Add the tool to the agent
To add the tool to the agent, use Studio and attach the table tool. Alternatively, use the ABL file to add the tool.
```yaml theme={null}
TOOLS:
find_orders(status: string) -> object
description: "Find customer orders by status"
type: table
table: customer_orders
scope: project
operations: [query]
query_template: "SELECT order_id, status, total FROM customer_orders WHERE status = :status LIMIT 10"
```
For all Table tool properties, see the [Tools reference documentation](/agent-platform/abl/reference/tools#table-tools).
## Best Practices
1. **Use placeholders for values.** Always write `:paramName` in SQL statements. Never build a statement from user text.
2. **Grant the fewest operations.** Enable only what the agent needs. Prefer read-only tools.
3. **Choose the right scope.** Use End user for personal data, Session for temporary data, and Project only for shared data. Agent writes to Project tables are audited.
4. **Mark personal data as Sensitive.** Do this for phone numbers, IDs, and similar fields.
5. **Index the columns you search or sort by.** Queries on non-indexed columns are rejected.
6. **Describe every parameter.** Clear descriptions help the agent pass the right value.
7. **Validate inputs.** Have the agent confirm values with the user before it writes them.
8. **Test before you publish.** Use the Query tab to test the table, and test the tool with realistic parameter values, including missing ones.
# Analytics and insights overview
Source: https://koreai.mintlify.app/agent-platform/analytics-and-insights
**Insights** tells you how your agents perform: how conversations end, how well the agent handles them, how customers react, and what it all costs. The section spans executive reporting, quality evaluation by dimension, customer voice, channel diagnostics, ad hoc exploration, and the pipelines that produce the underlying scores.
The documents below group pages by subject rather than by screen, so one document often covers more than one page in the left navigation. Use the middle column to find the document that covers the page you're on.
| Document | Pages covered | When to use it |
| :- | :- | :- |
| [Usage insights](/agent-platform/analytics-insights/executive-dashboard-usage) | At a Glance, Billing and Usage | Start here. Gives the outcome, quality, sentiment, and cost position for a period, and reconciles what the project consumed with what it billed. |
| [Quality insights](/agent-platform/analytics-insights/performance-quality) | Quality Monitor, Agent Performance | Open when the quality figure on At a Glance moves and you need to know which aspect of quality slipped and which agent caused it. |
| [Customer insights](/agent-platform/analytics-insights/customer-voice) | Customer Insights, Feedback | Open when you need the customer's side of the conversation: what they asked for, how their sentiment moved, and what they said in their own words. |
| [Voice analytics](/agent-platform/analytics-insights/voice-analytics) | Voice Analytics | Open when voice conversations underperform their chat equivalents. Separates agent reasoning problems from call quality, speech recognition, and latency problems. |
| [Agent transfer](/agent-platform/analytics-insights/agent-transfer) | Agent Transfer | Open when escalation volume concerns you more than the agent's own handling, and you need queue and human handling performance for transferred conversations. The page appears only after you connect Kore SmartAssist to the project. |
| [Analytics pipelines](/agent-platform/analytics-insights/analytics-pipelines) | Pipelines | Open to see which evaluations run today, or to define your own evaluation logic when the built-in dimensions don't score what your business cares about. |
| [Analytics explorer](/agent-platform/analytics-insights/analytics-explorer) | Analytics Explorer | Open when the prepared pages don't answer your question. Gives near real-time event, model, token, and cost data, session and trace drill-down, and custom queries over project event data. |
## Before you begin
Insights pages report on data that other parts of the platform produce, so several of them stay empty until you put the source in place.
* You need at least **Viewer** access to the project.
* **Quality Monitor**, **Agent Performance**, and **Customer Insights** depend on analytics pipelines. Enable pipelines in **Settings** before you expect scores on these pages.
* **Voice Analytics** needs at least one voice channel deployment. Without one, the page has no calls to report on.
* **Agent Transfer** needs an active Kore SmartAssist connection that your Agent Transfer settings select. Until then, the page doesn't appear in the navigation at all.
* Metric cards across the section show a dash until a pipeline evaluates enough conversations in the selected period. A dash means the measure has no data, not that it scored zero.
# Agent transfer
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/agent-transfer
**Agent Transfer** reports on conversations that leave the AI agent and reach a person. It covers transfer efficiency by channel, queue wait times and abandonment, and the performance of the human agents who handle escalated conversations.
Use it when escalation volume concerns you more than the AI agent's own handling. The quality pages tell you how well the agent performed before it escalated. This page tells you what happened afterward.
## Page availability
This page appears only when the project connects to Kore SmartAssist. Unlike the rest of Insights, it depends on an integration you set up yourself, so a project that never configured SmartAssist won't show the page at all.
### Requirements
The page appears once both of the following hold true.
| Requirement | Where you set it |
| - | - |
| The project holds a Kore SmartAssist connection with an active status. | **Project** -> **Integrations** |
| The Agent Transfer settings select that same connection. | **Project** -> **Settings** -> **Agent Transfer** -> **Connection** |
Both requirements point to the same connection. An active SmartAssist connection that your Agent Transfer settings don't select doesn't satisfy the second requirement, which matters in projects that carry more than one SmartAssist connection from an import or an earlier setup.
### Behavior before setup completes
Until the project meets both requirements, the platform hides the page rather than showing it empty. **Insights** omits it from the navigation, universal search doesn't return it, and opening its URL shows setup guidance in place of the dashboard.
Four situations produce that setup state:
* The Agent Transfer settings select no connection.
* The settings select a connection that no longer exists.
* The settings select a connection that isn't a Kore SmartAssist connection.
* The settings select a SmartAssist connection whose credentials expired or that someone revoked.
The last situation also removes a page you could previously reach. If Agent Transfer disappears from a project that used it, check the connection status before you check anything else.
### What the gate doesn't affect
The gate covers this dashboard only. Escalation routing, SmartAssist webhooks, and customer satisfaction collection all continue to run whether or not the page appears, so a hidden page never means that transfers stopped reaching your agents.
### A visible page that reports an error
A connection can satisfy both requirements and still carry incomplete credentials or metadata. In that case the page appears and reports a connection error in place of metrics.
Read the two states differently. A missing page means the setup isn't complete. A visible page reporting a connection error means the setup is complete but the credentials behind it need attention.
## Navigation
**Navigation**: **Project** -> **Insights** -> **Agent Transfer**
A date range control at the top of the page scopes every section below it.
## Page sections
The page divides into three sections, each answering a different question about the same escalated conversations.
| Section | Description |
| - | - |
| **Efficiency** | Reports transfer counts, average handling time, and resolution rates, split across voice, chat, and overall transfers. Read it to see whether escalations concentrate in one channel, which usually points to a coverage gap in that channel rather than a general escalation problem. |
| **Queue Performance** | Reports average wait time, longest wait time, abandonment rate, and handling rate for each queue. Use it to separate a staffing problem from a demand problem. Long waits and high abandonment in one queue point to staffing, while waits that spread evenly across queues point to volume the whole team can't absorb. |
| **Agent Performance** | Reports conversations handled, average handle time, resolution rate, and customer satisfaction for each human agent who takes escalations. |
The **Agent Performance** section here measures the people who receive escalations. The **Agent Performance** page under **Quality & Performance** measures the AI agents that raise them. The two report on different populations and different scales, so don't read one as a continuation of the other.
# Analytics explorer
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/analytics-explorer
The **Analytics Explorer** page monitors event volume, LLM performance, token consumption, and cost in near real time. Use it to investigate production incidents, track the impact of model changes, or audit LLM spend during peak traffic.
**Navigation**: **Project** -> **Insights** -> **Analytics Explorer**
**Date range**: Set at the upper-right area of the page, for example **Last 7 days**. Every tab follows this range.
**Filters**: Select **All environments** and **All channels** at the top of the page to narrow the data by deployment environment and channel. Filter selections are reflected in the page URL, so you can bookmark a filtered view or share it with a colleague.
The page organizes its detail into six tabs.
| Tab | What it shows |
| - | - |
| **Overview** | Headline volume, error, token, and cost metrics for the selected period. |
| **LLM Performance** | Call volume, latency, token, and cost metrics, broken down by model. |
| **Sessions Explorer** | Session-level records with filters, columns, and export. |
| **Traces Explorer** | Individual trace events, with timeline and waterfall views. |
| **Generations** | Every recorded model call, with latency, token, cost, and credential detail. |
| **Query** | Ad-hoc SQL queries on project event data. |
## Overview
Six metric cards summarize the selected period at a glance.
| Metric | Description |
| - | - |
| **Sessions** | Total sessions in the selected period. A session is a single end-to-end interaction between a user and the agent system. |
| **Messages** | Total messages across all sessions, including both user messages and agent responses. |
| **LLM Calls** | Total LLM API calls agents made during the period. Includes calls to all configured models, for example routing, generation, and evaluation. |
| **Errors** | Total errors during agent execution, including LLM failures, timeout errors, and tool invocation errors. |
| **Tokens** | Total LLM tokens, both input and output, across all calls. |
| **Cost** | Estimated cost based on token usage and per-model pricing. Use it to track spend relative to budgets or compare cost-efficiency across model configurations. |
## LLM performance
**LLM Performance** reports call volume, latency, tokens, and cost for the model calls in the selected period, broken down by model. Use it to benchmark models and to find the ones that drive latency or spend.
Five metric cards summarize the period.
| Metric | Description |
| - | - |
| **LLM Calls** | Total model calls in the selected period. |
| **Avg Latency** | Mean time a model call took to complete. |
| **P95 Latency** | Latency that 95% of calls finished within. Read it alongside the average, because a few slow calls can raise it while the average stays flat. |
| **Tokens** | Total input and output tokens across all calls. |
| **Cost** | Estimated cost based on token usage and per-model pricing. |
### Charts
Two charts show how latency and cost develop across the period.
| Chart | Description |
| - | - |
| **Latency Trend** | Plots call latency across the selected period, so you can spot when calls slowed down. |
| **Cost by Model** | Shows each model's share of total cost, with a legend that lists the models in the period. |
### Model performance
The **Model Performance** table lists one row per model, sorted by **Cost** in descending order, so the models that cost the most appear first.
| Column | Description |
| - | - |
| **Model** | Model that served the calls, for example `gpt-realtime-1.5`. |
| **Provider** | Provider of the model, for example `openai`. |
| **Calls** | Number of calls the model served. |
| **Tokens** | Total tokens the model's calls consumed. |
| **Cost** | Estimated cost of the model's calls. |
**LLM Performance** groups calls by model, not by credential. To see which credential or auth profile supplied provider access for a call, use the **Credential Name** column on the [Generations](#generations) tab.
## Sessions explorer
**Sessions Explorer** lists every session in the selected period so you can isolate the ones worth investigating. Four metric cards summarize the set.
| Metric | Description |
| - | - |
| **Total Sessions** | Sessions recorded in the selected period. |
| **Active** | Sessions currently in progress. |
| **Avg Duration** | Mean session duration across the set. |
| **Error Rate** | Percentage of sessions that recorded at least one error. |
### Narrow the list
Status chips filter the list to **All**, **Active**, **Completed**, **Escalated**, **Failed**, or **Ended**. Below the chips, five controls refine the results further.
| Control | Description |
| - | - |
| **Search sessions** | Matches sessions by keyword. |
| **Channel** | Filters by the channel the session arrived on. Defaults to **All channels**. |
| **Environment** | Filters by deployment environment: `development`, `staging`, `production`, or `working-copy`. Defaults to **All environments**. |
| **Source** | Filters by session source. Defaults to **All sources**. |
| **Filters** | Opens additional filter options. |
Select **Columns** to choose which columns the table displays. Select **Export** to download the session list.
Sessions started from a project's working copy, the pre-deployment draft, are recorded under the `working-copy` environment. Filter it out to keep pre-deployment testing from skewing your production metrics, or select it to review draft behavior on its own.
### Session list
| Column | Description |
| - | - |
| **Session ID** | Unique session identifier, with a copy control. |
| **Agent** | Agent that handled the session, for example `MainSupervisor`. |
| **Status** | Current session state, for example `ended`. |
| **Environment** | Environment the session ran in. |
| **Last Activity** | Time since the last recorded activity. Sorts the table by default. |
| **Traces** | Number of trace events the session generated. |
| **Duration** | Total session duration. |
| **Tokens** | Total tokens the session consumed. |
| **Cost** | Estimated cost for the session. Values below the display threshold show as `<$0.001`. |
| **Errors** | Error count for the session. Non-zero values are highlighted. |
Expand a row to see the full session ID, channel, trace event count, and last activity. Select **View Traces** to open that session in **Traces Explorer**.
## Traces explorer
**Traces Explorer** inspects the individual events inside a session.
### Filter traces
Type chips filter events to **All**, **LLM Call**, **Tool Call**, **Decision**, **Handoff**, **Error**, or **Agent**. Each applied filter appears as a chip below the search field, for example `Type: LLM Call`. Remove a single filter from its chip, or select **Clear all** to remove them all. **Reset filter** shows the number of active filters and restores the unfiltered list.
Use **Search traces** to match events by keyword, **Filters** for additional filter options, and **Export** to download the trace list.
### Sessions panel
The left panel lists the sessions in scope. Each card shows the agent name, status, span count, turn count, and duration. Select a session to load its events in the detail panel.
### Timeline and waterfall
The detail panel shows the selected session's events in one of two views. A counter reports how many events the current filter matches, for example `4 of 84 events`.
| View | Description |
| - | - |
| **Timeline** | Lists events in sequence as expandable cards. Use it to read an event's full payload. |
| **Waterfall** | Plots spans on a shared time axis, with each span's offset from the session start and its duration bar. Use it to find latency gaps and see where errors occurred. |
**Waterfall** opens with a summary bar reporting the trace mode (for example, **Historical**), **Spans**, **Duration**, **Tokens**, **Cost**, and **Errors**. Each span row shows a status icon for success or failure, the agent and model, the offset from session start, and the duration bar. Chips on the row repeat the span duration, token count, and cost.
### Event detail
Every event card carries the event type, agent name, and duration, plus `phase`, `run`, and `cause` badges. Expand a card for the full record.
| Field | Description |
| - | - |
| **Trace ID** | Identifier for the trace the event belongs to. |
| **Session ID** | Session the event belongs to. |
| **Span ID** | Identifier for this span. |
| **Event ID** | Identifier for this event. |
Each identifier has a copy control.
An **LLM Call** event adds model execution detail: **Model**, **Provider**, **Input Tokens**, **Output Tokens**, **Total Tokens**, **Cost**, and **Streaming**. Expand **Messages** to read the prompt and user turns, and **Response** to read the model output.
A **Tool Call** event shows the **Tool** name, an expandable **Input**, and **Show raw data** for the unformatted payload.
## Generations
The **Generations** tab lists every model call the platform recorded, drawn from a unified LLM call ledger. The ledger captures calls from across the platform in one place, including agent turns, pipeline runs, and guardrail evaluations, so you can account for model spend and latency across the whole project instead of one surface at a time. Each call also records the credential that supplied provider access, so you can attribute token consumption and cost to a specific credential or auth profile.
Use **Search generations** to match calls by keyword, **Filters** for additional filter options, **Columns** to choose which columns the table displays, and **Export** to download the list.
### Generation list
| Column | Description |
| - | - |
| **Model** | Model that served the call, for example `gpt-5.2`. |
| **Name** | Caller that issued the request: an agent name such as `MainSupervisor`, or an internal operation such as `pipeline_generate_text` or `guardrail_evaluation`. |
| **Status** | Outcome of the call, for example `Success`. |
| **Channel** | Channel the call originated on, for example `web_debug`. Shows `--` for calls not tied to a channel, such as pipeline runs. |
| **Source** | Part of the platform that issued the call. See [Sources](#sources). |
| **Latency** | Time the call took end to end. |
| **Credential Name** | Name of the credential or auth profile that supplied provider access for the call. See [Credential attribution](#credential-attribution). |
| **Provider** | Provider of the model that served the call, for example `openai`. |
| **Credential Type** | Kind of source that supplied provider access, for example `credential`. See [Credential attribution](#credential-attribution). |
| **Total Tokens** | Input and output tokens combined. |
| **Cost** | Estimated cost for the call. Values below the display threshold show as `<$0.001`. |
| **Start Time** | Date and time the call started. |
| **TTFT** | Time to first token. Shows `NA` when the call didn't report one. |
### Sources
The **Source** column shows a readable label rather than an internal identifier, so you can tell at a glance which part of the platform issued a call.
| Source | What it covers |
| - | - |
| **Runtime** | Model calls agents make while handling conversations. |
| **Runtime Internal** | Supporting model calls the runtime makes outside the agent's own turn. |
| **Pipelines** | Model calls made by analytics pipelines, such as LLM-as-judge evaluations. |
| **Guardrails** | Model calls made while evaluating content with guardrail policies. |
Filter by **Source** to separate what your agents spend from what evaluation and safety checks spend. Pipeline and guardrail calls run on every qualifying conversation, so they can account for a large share of total calls without appearing in any single agent's numbers.
### Credential attribution
**Credential Name** and **Credential Type** identify the source that supplied provider access for each call. Use them to see which credential or auth profile consumed tokens and cost when a project and its workspace use several model configurations side by side.
| Credential type | What it identifies | Credential name shown |
| - | - | - |
| `credential` | A named credential that a model connection uses. | The credential name. Credential names can't change, so the name always matches the name at the time of the call. |
| `auth_profile` | An auth profile that a model connection uses. | The auth profile's current name. When you rename an auth profile, every call it supplied, past and future, shows the new name. |
| `platform_credit` | Model access that the platform manages. | `platform-credit` |
| `direct_key` | A legacy model connection that stores a provider key directly, with no credential or auth profile. | `Direct-key` |
Credentials and auth profiles that share a name stay separate. A project-level credential, a workspace-level credential, and a credential in another project never combine, even when their names match.
The following rules decide which credential a call reports and how long it keeps it.
| Situation | Behavior |
| - | - |
| Calls recorded before attribution | Don't carry a credential. The platform doesn't backfill earlier calls. |
| Replacing a provider key | Create a new credential for the new key. The new credential becomes a separate source, and its usage doesn't merge with the old credential's. |
| Failed calls | Carry a credential on the same terms as successful calls. |
| Pipeline and guardrail calls | Carry a credential when they write to the LLM call ledger. |
| Deleting a credential or auth profile | Calls keep their credential name for as long as the platform retains the call. |
| Secrets | Attribution stores only the credential identity and name. It never stores provider keys or secret values. |
## Query
The **Query** tab runs SQL on project event data for investigations the pre-built views don't cover.
1. Select a table, for example `abl_platform.platform_events`.
2. Write your SQL, or select **Example Queries** to start from a template. Templates include a starter query for the feedback table, which returns ratings, comments, and the custom dimensions captured on each session.
3. Select **Execute**. Select **Clear** to empty the editor.
Queries use the same scoping tokens as dashboard widgets. Use `{tenantId:String}` and `{projectId:String}` for tenant and project isolation, and `{from:DateTime64(3)}` and `{to:DateTime64(3)}` to follow the selected time range. `{sessionId:String}` is optional. Results are capped at 1,000 rows.
The following query counts events by type and flags errors:
```sql theme={null}
SELECT
event_type,
count() AS cnt,
countIf(has_error = 1) AS errors
FROM abl_platform.platform_events
WHERE tenant_id = {tenantId:String}
AND project_id = {projectId:String}
AND timestamp >= {from:DateTime64(3)}
AND timestamp <= {to:DateTime64(3)}
GROUP BY event_type
ORDER BY cnt DESC
LIMIT 100
```
The results panel reports the row count and runtime, for example `Results — 20 rows (11ms)`. Select **Copy** to copy the result set, or **Add to dashboard** to save the query as a widget on a custom dashboard.
# Analytics pipelines
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/analytics-pipelines
This document covers the pipeline framework in Analytics and Insights: the built-in pipelines that power Agent Performance, Quality Monitor, and Customer Insights, and the visual node-based editor for building custom evaluation logic.
Navigation: **Project** → **Insights** → **Pipelines**
Each pipeline card displays its name, description, enabled/disabled status (green "Enabled" badge or gray "Disabled" badge), trigger count, and last processed timestamp (or "Never processed" if the pipeline hasn't run yet). Use the search bar at the top to find pipelines by name.
## Sections
| Section | Purpose |
| - | - |
| **Built-in** | Pre-configured pipelines that ship with the platform, ready to enable with a single toggle. |
| **Custom** | User-defined processing workflows created using the visual pipeline editor. |
| **Recent Runs** | Pipeline execution history with timestamps, durations, status (success/failure), and links to output data. |
| **Data** | Pipeline output data available for dashboard integration, export, and downstream consumption. |
## Built-in pipelines
The platform ships with eleven pre-built pipelines covering evaluation, classification, detection, and monitoring needs. Enable each pipeline with a single toggle — no configuration required for the defaults.
| Pipeline | Description |
| - | - |
| **Sentiment Analysis** | Per-message sentiment scoring with conversation-level trajectory analysis. Feeds the Sentiment Trajectory chart in Customer Insights. |
| **Intent Classification** | Classifies conversation intent using LLM analysis with customer-defined taxonomy or auto-discovery. Feeds the Intent Distribution chart in Customer Insights. |
| **Quality Evaluation** | LLM-as-judge quality evaluation with configurable rubric dimensions. Feeds the Quality Score in Agent Performance and Quality Monitor. |
| **Hallucination Detection** | Detects unsupported claims, self-contradictions, and factual accuracy issues in agent responses. |
| **Knowledge Gap Analysis** | Identifies gaps in knowledge base coverage by analyzing retrieval precision and uncovered topics. |
| **Guardrail Analysis** | Evaluates guardrail effectiveness — detects false positives, false negatives, and bypass attempts. |
| **Context Preservation** | Evaluates whether agents preserve relevant user and workflow context through the conversation. |
| **Friction Detection** | Detects user frustration signals — rephrased questions, message escalation, caps, and exclamation patterns. |
| **Anomaly Detection** | Monitors analytics metrics for statistical anomalies using z-score and SPC (Statistical Process Control) charts. |
| **Drift Detection** | Monitors analytics metrics for gradual performance drift by comparing baseline and current windows. |
| **Evaluation Run** | Executes persona × scenario × evaluator matrix evaluation with bias mitigation and trajectory scoring. |
## Custom pipelines
Custom pipelines let you define your own analytics logic using a visual, drag-and-drop node-based editor. Use custom pipelines to build organization-specific evaluation criteria that go beyond the built-in set — for example, regulatory compliance checks, brand voice adherence, or domain-specific accuracy scoring.
### How the editor works
The editor presents a visual canvas where you construct a pipeline as a directed graph of connected nodes. A **Node Palette** on the left side provides draggable node types organized into categories:
| Category | Node types |
| - | - |
| **Data** | Read Conversation, Read Message Window, Aggregate, Database Query, Filter, Transform Data, Inspect Output |
| **Logic** | Sub-Pipeline (reuse an existing pipeline as a step), Wait for Event (pause execution until a named event fires) |
The core pattern: define a trigger, connect one or more processing nodes, then wire the output to a storage or metrics node.
### Pipeline structure
| Component | Description |
| - | - |
| **Trigger** | Defines when the pipeline runs. Trigger types include Kafka events, scheduled intervals, in-platform events (for example, conversation completed), or filtered subsets of sessions. |
| **Processing nodes** | The evaluation and transformation logic applied to each triggered item. Nodes can read conversation data, query databases, aggregate metrics, filter records, transform data shapes, or call LLMs for evaluation. |
| **Metrics output** | The results the pipeline produces. Metrics take the form of named, typed values (counts, percentages, scores) that appear in the Data section and that you can wire into dashboards. |
### Pipeline lifecycle
Pipelines follow a Draft → Validate → Test → Activate lifecycle. The editor toolbar shows the current state (Draft badge, "Validation passed" indicator) and provides buttons for Test, Validate, Save, and Activate. You can iterate on draft pipelines without disrupting live ones, save drafts as you go, validate to catch configuration errors before deployment, and run test executions to verify output before activating.
### Sub-pipelines
You can reference one pipeline as a step inside another, enabling reuse and composability. For example, a "Compliance Evaluation" pipeline could call both the built-in "Guardrail Analysis" and a custom "Regulatory Wording Check" as sub-steps.
### Attach to dashboards
Once a custom pipeline produces data, its metrics appear alongside built-in metrics in **Agent Performance**, **Quality Monitor**, and **Customer Insights**, giving teams a single pane of glass across both standard and organization-specific evaluation.
Start with the built-in pipelines to establish baselines, then create custom pipelines for organization-specific quality dimensions. The validate-before-activate workflow catches configuration errors before pipelines go live.
# Custom dashboards
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/custom-dashboards
The **Custom Dashboards** page lets you build project-shared dashboards from governed SQL queries and visualizations. You create a dashboard, query your data with SQL against the platform tables, and save each visualization as a widget, every dashboard is shared across the project.
Navigation: **Insights** → **Custom Dashboards**
Date range: Set at the top of the dashboard, for example **Last 7 days**. Widgets that use the `from` and `to` date tokens follow this range, all applicable widgets update.
**Filters**: Select **All environments** and **All channels** at the top of the page to narrow the data by deployment environment and channel. Filter selections are reflected in the page URL, so you can bookmark a filtered view or share it with a colleague.
When a project has no dashboards, the page shows **No dashboards yet**. Select **New dashboard** to create your first one.
### Dashboard actions
| Action | Description |
| - | - |
| **New dashboard** | Creates an empty dashboard that you name and populate with widgets. |
| **Import** | Loads a dashboard definition from a file. |
| **Export** | Saves the selected dashboard's definition to a file, so you can move it between projects or keep a backup. |
| **Refresh** | Re-runs the queries behind the dashboard's widgets and redraws them with the latest data. |
| **Edit** | Opens the dashboard for changes, such as adding, arranging, or removing widgets. |
| **Time range** | Sets the dashboard date range that date-scoped widgets follow. |
### Build a widget
Each visualization on a dashboard is a widget. Select **Add widget** to open the widget editor, which opens as **Untitled widget** and stays open until you save or cancel.
| Control | Description |
| - | - |
| **Query data** | Selects the table and holds the SQL that returns your data. |
| **Preview rows** | Shows the rows the query returned, with result statistics. |
| **Visualize** | Turns the result set into a chart and configures how it displays. |
| **Save widget** | Saves the widget to the dashboard. |
| **Cancel** | Discards the widget and closes the editor. |
To build a widget:
1. Open a dashboard and select **Add widget**.
2. On **Query data**, select a table from the table selector, for example `abl_platform.platform_events`.
3. Write your SQL. Use the **Table columns** panel to find column names and data roles.
4. Select **Run query**. The result panel below the editor reports the rows returned.
5. Open **Preview rows** to confirm the query returns what you expect.
6. Open **Visualize**, choose a chart type, then map your fields to the axes.
7. Select **Save widget**.
#### Query data
The SQL editor sits at the center of the tab, with the table selector and **Run query** above it. Queries are scoped by `tenant_id` and `project_id` by default. Use the `from` and `to` date tokens when the widget should follow the dashboard date range. See [Example of Query scoping](#example-of-query-scoping).
The right rail supports the editor with three panels.
| Panel | Description |
| - | - |
| **Custom dimensions** | Lists the custom dimensions your project captures, so you can group and filter widgets by business-specific values. Custom metrics and dimensions now cover workflow sessions, from function nodes and agent nodes, also to the agent sessions. |
| **Viewer filters** | Lists the fields you can filter this widget on. Use **Find a field** to search, then select **Add filter to this widget**. A viewer filter shows on this widget only. Promote it to apply the filter across the whole dashboard. |
| **Table columns** | Lists every column in the selected table, with a data role badge of `id`, `time`, `dimension`, or `metric`. Use **Filter columns** to search long column lists. |
#### Result panel
After you run a query, the panel below the editor holds the result. Switch between **Preview rows** and **Visualization** without leaving the result, so you can check the data and the chart against each other.
The result reports five statistics.
| Statistic | Description |
| - | - |
| **Rows** | Number of rows the query returned. |
| **Columns** | Number of columns in the result set. |
| **Runtime** | Time the query took to execute, in milliseconds. |
| **Row cap** | Maximum rows a single fetch returns, for example `1,000 max`. Table widgets page through results beyond this cap. |
| **Range** | Date range the query ran against, for example `Last 7 days`. |
Below the statistics, a chip for each column shows the data role the platform assigned it, for example `timestamp: time` and `rows: metric`. These roles determine which columns are available to each axis in **Visualize**.
**Preview rows** displays the result as a table, one column per field in the result set.
#### Visualize
**Visualize** builds the chart from the result set. A status badge reads **Ready** when the current mapping produces a valid chart. Four tabs configure the visualization.
| Tab | Settings |
| - | - |
| **Charts** | Selects the chart type: **Line**, **Area**, **Bar**, **Combo**, or **Table**. Select **More charts** for additional types. **Table** widgets page through large result sets, so high-volume data isn't truncated at the first 1,000 rows. |
| **Fields** | Maps result columns to the chart. Set the **X-axis** column, then add one or more **Y-axis series**. Select **Add field** to plot another series, or the delete icon to remove one. |
| **Display** | Sets **Value format** and **Secondary Y axis** behavior, for example **Auto (split mixed scales)** when series use different scales. A **Current mapping** card shows the active X-axis and Y-axis series. |
| **Axes** | Sets the **X-axis label** and **Y-axis label**. Labels default to the mapped column names. |
### Example of query scoping
The platform scopes queries to the current tenant and project by default. Let us consider `abl_platform.platform_events` as the query table. Use the date tokens when a widget should follow the dashboard date range.
| Token | Description |
| - | - |
| `{tenantId:String}` | Scopes the query to the current tenant. |
| `{projectId:String}` | Scopes the query to the current project. |
| `{from:DateTime64(3)}` | Start of the dashboard date range. |
| `{to:DateTime64(3)}` | End of the dashboard date range. |
The following query counts events over time and follows the dashboard date range:
```sql theme={null}
SELECT
timestamp,
count() AS rows
FROM abl_platform.platform_events
WHERE tenant_id = {tenantId:String}
AND project_id = {projectId:String}
AND timestamp >= {from:DateTime64(3)}
AND timestamp <= {to:DateTime64(3)}
GROUP BY timestamp
ORDER BY timestamp DESC
LIMIT 100
```
# Customer voice
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/customer-voice
This document covers the customer experience layer in Analytics and Insights: Customer Insights for intent classification, sentiment trajectory, and frustration detection; Feedback for end-user ratings and verbatim comments; and Feedback Insights for the results of an individual feedback survey.
## Customer insights
The **Customer Insights** page helps you understand what customers are asking about and how they feel about the experience. It combines intent classification, sentiment scoring, frustration detection, and resolution tracking into a single view to help you identify emerging topics, detect dissatisfaction early, and measure whether the agent resolves the intents it encounters.
Navigation: **Project** → **Insights** → **Customer Insights**
**Date range selector**: Use the toggle to select **7d**, **30d**, or **90d**.
**Filters**: Select **All environments** and **All channels** at the top of the page to narrow the data by deployment environment and channel. Filter selections are reflected in the page URL, so you can bookmark a filtered view or share it with a colleague.
### Customer sentiment summary
A banner at the top displays a contextual summary of the analyzed data, for example: "322 analyzed conversations · 144 intent-classified · 322 sentiment-scored · 13 intents detected · 26.7% frustration." A sentiment indicator (for example, "Mixed Sentiment") provides a quick qualitative read.
### KPI metric cards
| Metric | Description |
| - | - |
| **Analyzed Conversations** | Total conversations that pipelines analyzed in the selected period. The sub-label shows the breakdown. |
| **Unique Intents** | Number of distinct intents the system identified across all analyzed conversations. |
| **Avg Sentiment** | Average sentiment score across all conversations. A score near 0 indicates neutral sentiment; positive values indicate positive sentiment. |
| **Frustration Rate** | Percentage of conversations where the system detected user frustration signals (repeated questions, negative language, escalation requests). A warning triangle appears when the rate exceeds the configured threshold. |
| **Resolution Rate** | Percentage of conversations reaching successful resolution. A warning triangle appears when the rate drops below the threshold. The sub-label shows the count of resolved conversations the system evaluated. |
### Intent distribution
A horizontal bar chart ranks detected intents by volume. Each bar shows the intent name, count, and percentage of total conversations. The chart groups low-volume intents (typically those below a threshold count) into an "Other" category. The footer indicates the total classified intent assignments and how many low-volume intents the chart grouped.
### Sentiment trajectory
A side-by-side panel displays conversation counts across three sentiment directions:
| Direction | Description |
| - | - |
| **Improving** | Conversations where sentiment trended positively over the course of the interaction. |
| **Stable** | Conversations where sentiment remained consistent throughout. |
| **Declining** | Conversations where sentiment deteriorated — candidates for investigation. |
The footer indicates the total conversations with sentiment data contributing to the trajectory analysis.
### Trends over time
A section below the distribution charts plots intent volumes and sentiment scores as time-series data, enabling you to track whether specific intents are growing or shrinking and whether sentiment is trending in the right direction.
***
## Feedback insights
The **Feedback Insights** page reports the results of a single feedback survey. Use it to read the headline score, see how the score moved, find the segments driving it, and open the individual responses behind it.
Feedback Insights reads the responses collected against a [feedback template](/agent-platform/feedback-template). Responses reach the page from agents that reference the template and from external systems that submit through the public feedback API. Until you create a template, the survey list offers only the built-in `message_feedback` capture, and the page prompts you to set up a survey before it reports anything.
**Navigation**: **Project** → **Insights** → **Feedback Insights**
**Date range selector**: Use the toggle to set the reporting window, for example, **Last 7 days**.
**Filters**: Select **All environments** and **All channels** at the top of the page to narrow the data by deployment environment and channel.
**Export**: Select **Export CSV** to download the current results for offline analysis.
### Survey and score method
Two controls at the top of the page define every figure below them.
| Control | Description |
| - | - |
| **Survey** | Selects the survey to report on. The list contains every feedback template in the project, plus the built-in `message_feedback` capture. Every figure on the page belongs to the survey you select. |
| **Scored as** | Selects how the page reads the recorded ratings. Changing the score method changes the figure, never the underlying data. |
### Overview
The **Overview** tab reports the headline score for the selected survey, the comparison against the prior period, and the count of externally submitted responses.
Below the score, the response funnel shows how many of the people who met the survey went on to answer it.
| Stage | Description |
| - | - |
| **Presented** | Surveys rendered in a conversation, whether or not anyone answered. |
| **Scored** | Responses where the user submitted a rating. This is the denominator for every score on the page. |
| **Commented** | Responses that included comment text. Reads as not applicable when the survey doesn't ask for comments. |
| **Abandoned** | Surveys presented and not answered inside the selected window. |
The funnel also reports two rates. The response rate needs a presented count, and reads as not available until the survey records one. The comment rate reads as not applicable when the survey doesn't ask for comments.
Surveys have no answer window, so a late response can still turn an abandoned survey into a scored answer.
#### Read the margin before the number
The page styles a change as movement only when the change clears the combined margin of both periods.
An index built from two proportions carries roughly twice the margin of a plain percentage at the same sample size. NPS is such an index: it subtracts the share of detractors from the share of promoters. At the same number of responses, the same visible gap on an NPS figure means less than it would on a plain percentage.
### Trend and distribution
The **Trend & distribution** tab shows how the score moved across the selected period, and how the responses spread across the rating scale. Use the granularity control on the **Trend** panel to change the interval, for example, from daily to a wider grouping.
### Segments
The **Segments** tab breaks the score down by the dimensions the platform records with each response.
| Segment | Compares scores across |
| - | - |
| **Agent** | The agents that presented the survey. |
| **Rendered language** | The language the survey rendered in. |
| **Version** | The project version in use when the response came in. |
Select **Movers first** to order segments by the size of their change. The page orders segments by significance, so a segment with few responses can't headline the list on a dramatic-looking number alone.
### Responses
The **Responses** tab lists the individual responses behind the figures on the other tabs. Four controls let you narrow the list.
| Filter | Description |
| - | - |
| **All agents** | Filter by the agent that presented the survey. |
| **All ratings** | Filter by a specific rating value. |
| **All languages** | Filter by the language the survey rendered in. |
| **Search comments** | Match text within submitted comments. |
If no responses match, the tab reports that it found no feedback for the selected filters. Widen the date range or clear a filter.
# Usage insights
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/executive-dashboard-usage
This document covers the two pages that answer how the program performs and what it costs.
* **At a Glance** reports conversation outcomes, quality, sentiment, and return on investment for a period.
* **Billing and Usage** reports what the project consumed and which part of that consumption bills.
Both pages report on conversations that evaluation pipelines have already processed. Until a pipeline runs, the metric cards show dashes rather than zeros.
## At a Glance
**At a Glance** aggregates outcome, quality, sentiment, and cost metrics into a single view that needs no configuration. Use it for a daily check or a leadership review, and to decide which of the deeper pages to open next.
**Navigation**: **Project** -> **Insights** -> **At a Glance**
### Page filters
Three controls at the top of the page scope everything below them: an environment filter, a channel filter, and a date range.
Narrow the environment filter when production and non-production traffic would otherwise mix. Test conversations distort containment and cost figures more than any other input.
When you change a filter, a **Reset filter** control appears with the number of filters you applied. Check it before you conclude that a period was quiet, since a narrowed view looks much the same as a slow week.
### Metric cards
Six cards summarize the selected period. Each card shows a headline value and a supporting figure that supplies its denominator. Read the two together, because a containment rate means one thing across a thousand conversations and something else across one.
| Metric | Description |
| - | - |
| **Conversations** | Counts total conversations in the period and shows how many of them pipelines evaluated. The evaluated count forms the sample that produces every other card on this page, so a wide gap between the two figures means the rest of the view describes only part of your traffic. |
| **Containment Rate** | Shows the share of evaluated conversations the agent resolved without handing off to a person. Most programs treat this as their headline automation measure. The card takes a warning treatment when the rate falls below the expected threshold. |
| **Quality Score** | Averages the scores the quality evaluation assigns to the agent's responses. Use it as the counterweight to containment. Containment alone can rise because the agent stopped offering escalation, and quality tells you whether the agent handled the conversations it kept. |
| **Avg Sentiment** | Averages sentiment across evaluated conversations. Quality judges what the agent produces, and sentiment reflects how the customer responds to it. The two measures can diverge, and the gap between them usually carries the signal. |
| **Period Savings** | Models the cost you avoid when the agent contains a conversation instead of a person handling it. The figure rests on the assumptions you set in ROI cost settings, so treat it as directional and revisit it when your staffing costs change. |
| **Escalation Rate** | Shows the share of evaluated conversations the agent handed to a person. This rate isn't the inverse of containment. Conversations the agent handled only in part and didn't escalate count toward neither rate, so both can read low over the same period. |
A card shows a dash when the pipeline behind it produces no value for the period. A dash means the measure has no data, not that it scored zero, and it usually indicates that the relevant pipeline isn't running.
### Overview tab
The **Overview** tab explains the headline numbers.
**Conversation Volume & Containment Rate** plots volume and containment on one timeline. The chart separates the two ways a containment figure moves: the agent resolves a different share of conversations, or the volume changes underneath a stable share.
**Outcome Distribution** divides the same conversations into three categories. Each category appears as a segment with its count, its share of the total, and a caption that describes the outcome. A footer states how many conversations the distribution covers.
| Outcome | Description |
| - | - |
| **Resolved** (green) | The agent completed the conversation without involving a person. |
| **Contained partial** (amber) | The agent handled the conversation but didn't carry it to a full resolution. These conversations count toward neither the containment rate nor the escalation rate, so both rates can read low over the same period. |
| **Escalated** (red) | The agent handed the conversation to a person. |
The middle category is the reason to read this section rather than the cards alone. Containment and escalation together imply a binary outcome, and the partial category holds the conversations that fit neither.
**What's Driving the Metrics** attributes the period to individual agents, so you can locate the cause of a project-level movement.
| Column | Description |
| - | - |
| **Volume** | Counts the evaluated conversations that belong to this agent. Sort on this column to find the agents that carry enough weight to move the project figures, since a poor score across a handful of conversations rarely explains an aggregate shift. |
| **Confidence** | Shows the confidence that accompanies this agent's evaluation results. The column stays blank until the pipeline produces a confidence value. |
| **Quality** | Shows this agent's own quality score, so you can see whether the project average describes your agents evenly or conceals a spread. |
| **Sentiment** | Averages customer sentiment across this agent's conversations. |
| **Trend** | Shows the direction of change from the preceding period. The column stays blank until the project accumulates enough history to compare. |
### Trends tab
The **Trends** tab answers questions about direction rather than current state. Three charts cover the period.
**Multi-Metric Trends** plots containment, escalation, and quality on one timeline. Reading the three together is what makes the tab useful, because a containment rise that coincides with a quality fall is a different event from one that doesn't.
**Sentiment & Quality Over Time** plots average quality and average sentiment. Point to any date to read its values.
**Daily Outcome Distribution** shows the outcome mix day by day. Use it to tell whether a change in the period totals comes from a shift in how conversations end or simply from more of them.
### ROI tab
The **ROI** tab converts contained volume into money and staffing. Every figure except cost per conversation rests on the assumptions in cost settings, so agree those with your finance team before you circulate anything from this tab.
| Metric | Description |
| - | - |
| **Period Savings** | Shows the modeled human cost you avoided, net of what the AI spent. |
| **Period ROI** | Expresses that saving as a return on your AI spend. The card takes a warning treatment when the period produces no positive return, which is normal early in a deployment while volume stays low and the assumptions stay fixed. |
| **FTE Equivalent** | Restates contained volume as human staffing capacity and shows the equivalent monetary capacity underneath. Use it in headcount discussions, where an FTE figure lands better than a savings total. |
| **Cost / Conversation** | Divides actual AI cost by conversations. This card reports real spend rather than your assumptions, so quote it when someone disputes the assumptions. |
**AI Cost vs Estimated Human Cost** plots both costs across the period and states the per-conversation human cost it applies, so anyone who reads the chart can see the assumption behind the comparison. The platform attributes actual model costs to contained conversations.
### Conversations tab
The **Conversations** tab lists the individual conversations behind the metrics. Select a conversation to open its session record, which holds the full trace, cost, and message history. Use this tab when a metric looks wrong and you need to read the conversations that produced it.
### ROI cost settings
Select **Cost Settings** on the **ROI** tab to set the assumptions that produce the ROI figures, then select **Save**. Nothing here affects what the platform bills you. These values only determine the baseline that the platform compares your actual spend with.
| Field | Description |
| - | - |
| **Human Cost per Conversation (\$)** | Sets the loaded cost of one conversation that a person handles. The platform credits every containment with avoiding this amount, so this field drives the savings figures and the human cost line on the ROI chart more than any other input. |
| **Human FTE Monthly Cost (\$)** | Sets the fully loaded monthly cost of one full-time agent. The platform uses it to convert avoided volume into the FTE Equivalent figure. |
| **Avg Human Handle Time (minutes)** | Sets the staffed time that one conversation consumes. With the FTE cost, it determines how much contained volume adds up to one person's capacity. |
| **Estimated AI Cost per Conversation (\$)** | Sets the fallback cost the platform applies when it can't attribute actual model cost to a conversation. Without this value, unattributed conversations appear to cost nothing and overstate your savings. |
***
## Billing and usage
**Billing and Usage** shows what the project consumed, how much of that consumption bills, and the policy that decides the difference. Finance and operations teams use it to reconcile spend and to explain gaps between activity and charges.
**Navigation**: **Project** -> **Insights** -> **Billing & Usage**
### Data currency
The page reads published billing report rows rather than live traffic, and it states when the platform last published those rows. Check that timestamp before you reconcile a period, because recent activity appears only after the next materialization. A date range control scopes the page.
### Usage and billing units
| Metric | Description |
| - | - |
| **Included Sessions** | Counts the sessions that meet the billing policy and therefore contribute to charges. This number falls below your total session count whenever exclusions apply, and the page itemizes the difference further down. |
| **Billing Units** | Counts the chargeable units the platform derives from included sessions using the billing interval and the duration basis. This number maps to your invoice. |
| **LLM Calls** | Counts model invocations in the period. This card can show substantial activity while billing units sit at zero, because calls inside excluded sessions still run. Treat a wide gap between the two as a prompt to check your exclusions. |
| **Interactive Turns** | Counts user turns across the period. The interaction threshold in the billing policy uses turn count to decide whether a session carries enough interaction to bill. |
**Daily Billing Units** plots billing units and included sessions across the period, so you can trace a spike in charges to either more sessions or longer ones.
**Channel Breakdown** reports sessions, units, and model calls per channel. It shows which channel drives your spend, and it exposes an excluded channel as activity that produced no units.
**Voice minutes** reports voice consumption separately, because voice meters in minutes rather than in the unit model that governs the rest of the page.
### Excluded sessions
This section lists the sessions the latest materialization didn't bill, grouped by the reason that excluded each one.
Start here when session volume and billed volume disagree. The reason names the policy rule that removed the sessions, which turns an unexplained gap into a policy question.
### Usage by billing scope
This section groups sessions by billing scope. When one agent delegates to another, the parent and its children roll up into a single scope, so the platform counts a multi-agent conversation once rather than once per agent.
Read this section when your agent architecture makes the session count look higher than the work the customer experienced.
### Resolved billing policy
This section shows the billing policy in effect for the tenant and the level that sets each value. The source label matters most: it tells you whether a value comes from your plan or from this project, and therefore whether changing it needs your account team or your own settings.
| Setting | Description |
| - | - |
| **Billing interval (minutes)** | Sets the block of session time that one billing unit covers. It determines how session duration rounds into units, so a long interval and short sessions together decide how much of what you pay for goes unused. |
| **Duration basis** | Selects which measure of session duration the platform meters. It decides whether idle time inside a session becomes chargeable. |
| **Scope consolidation** | Controls whether the roll-up of delegated agent sessions applies to billing or only to reporting. It determines whether multi-agent fan-out costs more than a single-agent conversation. |
| **Excluded channels** | Lists the channels whose sessions never bill. Debug and test channels usually sit here, which explains how development traffic generates model calls without generating charges. |
| **Interaction threshold (min user messages)** | Sets the minimum number of user messages a session needs before it bills. It keeps abandoned and no-touch sessions out of your charges. |
# Quality insights
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/performance-quality
This document covers agent evaluation in Analytics and Insights.
* **Quality Monitor** reports quality by evaluation dimension across the project.
* **Agent Performance** reports the same measures agent by agent.
The two pages answer different questions. Quality Monitor tells you which aspect of quality slipped. Agent Performance tells you which agent caused it.
Both pages populate only after evaluation pipelines process conversations. A measure with no evaluations shows a dash rather than a zero.
## Quality monitor
**Quality Monitor** aggregates the output of the evaluation pipelines into one view, scored by dimension. Use it to decide where quality work is worth doing, rather than to judge how a single conversation went.
**Navigation**: **Project** -> **Insights** -> **Quality Monitor**
### Page filters
Environment, channel, and date range controls at the top of the page scope everything below them.
Filter by environment before you read the scores. Evaluations of test traffic and evaluations of production traffic answer different questions, and an average of both tells you neither.
### Quality health
A banner across the top gives three figures: the number of conversations the pipelines evaluated, a weighted overall score that combines the dimensions, and a count of dimensions in each status.
The three statuses are **Critical** (red), **Warning** (amber), and **Healthy** (green).
Read the status counts first. They tell you how many dimensions need attention without your having to interpret each card. A dimension with no data also counts toward the total, so a warning count can reflect missing coverage as readily as poor performance.
### Dimension cards
Each card reports one evaluation dimension for the period. Higher values are better on every dimension, so a low score always signals a problem rather than a threshold you need to interpret.
| Dimension | Description |
| - | - |
| **Overall Quality** | Combines the scores of the dimensions that hold data. Track it for direction over time. It runs too coarse to act on directly, which is the job of the remaining cards. |
| **Faithfulness Score** | Measures how well responses stay grounded in the sources the agent has available. A low score means the agent produces claims it can't support, the failure mode that costs the most trust when customers notice it. |
| **Knowledge Coverage** | Measures how often the knowledge base supports the query the agent receives. This dimension scores your content rather than your agent, so a low score tells you to write documentation instead of changing prompts. |
| **Safety Score** | Measures the rate at which responses pass the configured guardrails. It moves only when guardrails exist, so a persistent dash usually means you have no guardrails in place rather than that the platform checked nothing. |
| **Context Preservation** | Measures how well the agent carries context across turns in one conversation. It degrades first as conversations grow longer, and it usually explains complaints about customers repeating information. |
A dash means the pipeline behind that dimension produced no value for the period. It doesn't mean the dimension scored zero.
### Quality trend
The trend chart plots each dimension as its own series over the period.
Use it for correlation. Dimensions rarely move alone, and a fall in one alongside a rise in another usually points to a single underlying change, such as new traffic the knowledge base doesn't yet cover.
### Dimension details
Below the chart, each dimension appears with its score and a **Critical**, **Warning**, or **Healthy** badge.
Expand a dimension to see the component measures that produced its score. The expansion separates a dimension that one weak measure pulls down from a dimension that runs weak throughout, and the distinction matters because the first needs a targeted fix and the second usually doesn't.
Dimensions with no evaluations in the period carry a no-data badge in place of a status, so you can tell an unmeasured dimension from a failing one.
## Agent performance
**Agent Performance** reports the evaluation measures per agent. Open it once Quality Monitor tells you a dimension slipped. It earns its place in multi-agent projects, where one weak agent can pull down a project average that otherwise looks acceptable.
**Navigation**: **Project** -> **Insights** -> **Agent Performance**
### Page filters
Environment, channel, and date range controls scope the page.
Select **Compare previous period** to add a comparison with the period immediately before the one you selected. The comparison turns the scores from absolute values into a direction of travel. Use it before you draw conclusions from a single period, because evaluation scores move considerably on low volume.
### Agent health
A banner reports how many agents the pipelines evaluated, how many conversations that covered, and how many agents fall into each status: **Critical** (red) and **Healthy** (green).
It tells you whether you face a broad problem or a single agent, before you read any individual score.
### Metric cards
Five cards aggregate the measures across all agents in scope.
| Metric | Description |
| - | - |
| **Quality** | Averages the quality score across evaluated conversations on a five point scale. The table below reports the same measure per agent, so compare the two to see whether the average describes your agents evenly or conceals a spread. |
| **Hallucination Rate** | Reports the rate at which the pipeline flags responses for unsupported claims. Lower is better here, unlike the other cards, so read this card separately rather than scanning the row for low numbers. |
| **Knowledge Gaps** | Counts the conversations where the knowledge base couldn't support the query. It reports a count rather than a rate, so weigh it alongside conversation volume before you treat a rise as a regression. |
| **Safety Score** | Reports the rate at which responses pass the configured guardrails. |
| **Context Score** | Averages how well agents carry context across turns, on a five point scale. |
### Agent table
The table repeats these measures for each agent, alongside the agent's conversation count and an overall status that derives from its scores. The status appears as a **Critical** (red) or **Healthy** (green) badge.
Read the table with the conversation count in view. An agent that the pipeline evaluated on a handful of conversations can show the worst scores on the page without being the problem worth fixing.
To work through the table:
* Search by name to find a specific agent.
* Sort on status to bring the agents that need attention to the top.
* Switch between **Critical** and **All** using the filter pills to handle the failing agents first.
### Quality trend
The trend chart plots quality score and flagged conversation count on separate axes.
Read the two series together. Quality that falls while flags stay flat indicates a gradual drift in scoring. Quality that falls as flags rise points to specific conversations going wrong, which you can open and read.
# Voice analytics
Source: https://koreai.mintlify.app/agent-platform/analytics-insights/voice-analytics
This document covers voice channel monitoring in Analytics and Insights: call quality scoring, speech recognition accuracy, end-to-end latency tracking, barge-in rates, and DTMF fallback metrics for voice-enabled agents.
Navigation: **Project** → **Insights** → **Voice Analytics**
**Date range selector**: Use the toggle to select **Today**, **7d**, or **30d**.
**Filters**: Select **All environments** at the top of the page to narrow the data by deployment environment. Filter selections are reflected in the page URL, so you can bookmark a filtered view or share it with a colleague.
Channel filtering isn't available on this page. The underlying voice data source doesn't carry a channel dimension, so the channel selector is hidden here even though it appears on other analytics pages.
## KPI metric cards
| Metric | Description |
| - | - |
| **Total Calls** | Number of voice calls in the selected period. |
| **Avg MOS** | Average Mean Opinion Score for call quality on a scale of 1–5. Scores below 3.5 typically indicate noticeable quality issues. |
| **ASR Quality** | Automatic Speech Recognition quality score (0–100, higher is better). Measures how accurately the ASR engine transcribes the caller's speech. |
| **E2E Latency** | End-to-end latency in milliseconds for the voice processing pipeline. Covers the full round-trip from user speech input through ASR transcription, LLM processing, and TTS output back to the caller. |
| **Barge-In Rate** | Percentage of calls where the caller interrupted the agent mid-response. A rising barge-in rate may indicate that responses are too long or latency is too high, prompting callers to cut in. |
| **DTMF Fallback** | Percentage of calls falling back to touch-tone (keypad) input, typically when ASR fails to understand the caller. A rising rate may indicate ASR quality issues or unsupported accents/languages. |
## Trend charts
| Chart | Description |
| - | - |
| **Network Quality and Call Volume** | Dual-axis chart plotting MOS scores and call count trends over the selected period. Use this to correlate call quality dips with volume spikes — quality often degrades during peak traffic if capacity-constrained infrastructure cannot keep up. |
| **Speech Recognition Quality (ASR)** | ASR quality scores plotted over time. Monitor for sustained degradation, which may indicate noisy caller environments, model drift, or the introduction of new vocabulary that the ASR model does not yet recognize. |
Track **E2E Latency** trends after model or pipeline changes. Even small latency increases (50–100ms) can affect caller experience and drive up barge-in rates.
# List conversation messages
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-conversation-messages
agent-platform/api-specs/messages.yaml get /api/public/analytics/projects/{projectId}/conversation-history
Messages are ordered by time, then by message ID to keep the order stable.
Different filters combine with AND, and multiple values in the same filter
combine with OR.
A date-range query with no other filter that would span more than 10,000
sessions is rejected with `400`, so narrow the range or add `sessionIds` for
busy projects.
Returns the messages exchanged in a project's conversations chronologically. Message content is filtered according to the project's PII policy before it is returned, so sensitive values may be masked or removed. Messages are ordered by time, then by message ID to keep the order stable.
Different filters combine with AND, and multiple values in the same filter combine with OR.
Give either a date range (`fromDate` and `toDate` together) or a list of `sessionIds`. A date-range query with no other filter may span a long list of sessions, so always use a filter and paginate as needed. A response that span more than 10,000 sessions is rejected with `400`, so narrow the range or add `sessionIds` for busy projects.
For pagination, prefer `cursor`: pass the `nextCursor` value from the previous response. `offset` also works, and `skip` is an older name for `offset` that is still accepted. Use only one of the three in a single request.
Authenticate with the `x-api-key` header. The key must be bound to the project you are querying and have `analytics:read` permission.
# List conversation messages using JWE payload
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-conversation-messages-with-encrypted-payload
agent-platform/api-specs/messages.yaml post /api/public/analytics/projects/{projectId}/conversation-history
POST form of `GET` on this path. It accepts the same parameters as a flat JSON
body (`ConversationHistoryQueryBody`) instead of a query string, and returns the same response.
Use it when the request parameters must be encrypted. See "Sending parameters
in a POST body" and "Payload encryption" in the API description.
- Request encryption off: send `ConversationHistoryQueryBody` as `application/json`.
- Request encryption on: send the same object encrypted as an
`EncryptedRequest`, preferably as `application/jose+json`.
- Response encryption on: every response except `401` is an
`EncryptedResponse` with `X-ABL-Encrypted: true`.
Retrieve conversation messages while sending the request parameters in the request body instead of the URL query string. It supports the same filters, pagination options, and response format as [the corresponding GET endpoint](/agent-platform/api-reference/analytics-list-conversation-messages).
Use the POST endpoint when the request parameters **must be encrypted**. When request encryption is enabled for the Platform Key, send the request parameters as a JSON Web Encryption (JWE) payload. The encrypted payload protects the parameters that would otherwise be exposed in the GET query string.
Request encryption is configured on the Platform Key in Studio. The request uses a flattened JWE in JSON serialization and is encrypted using the AES-256 key associated with the Platform Key.
If response encryption is also enabled for the Platform Key, the endpoint returns encrypted responses, including error responses, except for `401 Unauthorized` responses. The response is a JWE encrypted for the RSA public key configured on the Platform Key.
# List LLM calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-llm-calls
agent-platform/api-specs/llm-ledger.yaml get /api/public/analytics/projects/{projectId}/llm-ledger
`fromDate` and `toDate` are required. `fromDate` is included in the results and
`toDate` is not, so a full day is
`fromDate=2026-08-04T00:00:00Z&toDate=2026-08-05T00:00:00Z`.
All other filters are optional and match exactly. Filters combine with AND, so
adding more narrows the results.
`source` and `sourceSubtype` also accept older spellings and translate them for
you — see those parameters for the details.
Use this endpoint to retrieve the LLM call ledger for a project. The response contains one record for each individual LLM call, including details such as the model, provider, token usage, latency, and cost. The endpoint does not aggregate calls by conversation turn.
Use the `dataMode` parameter to control the level of detail returned. The default mode returns call-level facts, while `full` additionally includes the exact prompt and response payloads when available.
The available filtering criteria are:
* Date range — required: `fromDate` and `toDate`. `fromDate` is inclusive, while `toDate` is exclusive.
* Source: Filter by the source of the LLM call.
* Source subtype: Further filter calls by source subtype.
* Model: Filter calls by the LLM model.
* Provider: Filter calls by the model provider.
* Other query parameters: The endpoint also supports pagination and sorting, but these control how results are returned rather than which records are included.
# List LLM calls using JWE payload
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-llm-calls-with-encrypted-payload
agent-platform/api-specs/llm-ledger.yaml post /api/public/analytics/projects/{projectId}/llm-ledger
POST form of `GET` on this path. It accepts the same parameters as a flat JSON
body (`LlmLedgerQueryBody`) instead of a query string, and returns the same response.
Use it when the request parameters must be encrypted. See "Sending parameters
in a POST body" and "Payload encryption" in the API description.
- Request encryption off: send `LlmLedgerQueryBody` as `application/json`.
- Request encryption on: send the same object encrypted as an
`EncryptedRequest`, preferably as `application/jose+json`.
- Response encryption on: every response except `401` is an
`EncryptedResponse` with `X-ABL-Encrypted: true`.
This endpoint retrieves the LLM call ledger for a project, with the request parameters supplied in the request body rather than the URL query string. It supports the same filters, pagination, sorting, and response formats as [the corresponding GET endpoint](/agent-platform/api-reference/analytics-list-llm-calls).
Use the `POST` endpoint when the request parameters must be encrypted. When request encryption is enabled for the Platform Key, send the LlmLedgerQueryBody as an encrypted JWE. When response encryption is enabled, the endpoint also returns the response as an encrypted JWE, including error responses after the API key has been authenticated.
The endpoint returns individual LLM calls rather than aggregating calls by conversation turn. You can use `dataMode` to return either call-level details or, with `full`, the stored prompt and response payloads when available.
# List session summaries
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-session-summaries
agent-platform/api-specs/sessions.yaml get /api/public/analytics/projects/{projectId}/sessions
Use `limit` and `offset` to page through the results. The response includes
message, trace-event, and token counts per session, but not the messages or
trace details themselves. Use the conversation-history endpoint for messages and
the traces endpoint for execution detail.
Use this endpoint to retrieve session-level analytics for a project. Each result represents one conversation session and includes summary information such as the channel, environment, status, containment outcome, message and trace-event counts, token usage, cost, and session duration. The response does not include the individual messages or trace details; use the conversation-history and traces endpoints to retrieve those details.
It provides the following filters:
* `fromDate` — Inclusive start timestamp.
* `toDate` — Exclusive end timestamp.
* `sessionIds` — Filter by specific session IDs; up to 10,000.
* `containmentType` — Filter by how the session was resolved, such as `contained_resolved`, `contained_partial`, `escalated`, or `abandoned`.
* `environment` — Filter by environment: `dev`, `staging`, `production`, or `working-copy`.
* `channelUId` — Filter by the end-user identity recorded on the channel.
* `callerNumber` — Filter by caller number or caller identity.
* `channel` — Filter by the channel through which the conversation originated.
* `traceDimensions[key]` — Filter by custom session dimensions and their values.
* `status` — Filter by session state, such as `active`, `idle`, `ended`, `completed`, `escalated`, or `abandoned`.
You must provide either `fromDate` and `toDate` together, or `sessionIds`. All other filters are optional. Multiple filters are combined with `AND` logic; multiple values within a filter are combined with `OR` logic.
Use `limit` and `offset` to paginate through the results.
# List session summaries using JWE payload
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-session-summaries-with-encrypted-payload
agent-platform/api-specs/sessions.yaml post /api/public/analytics/projects/{projectId}/sessions
POST form of `GET` on this path. It accepts the same parameters as a flat JSON
body (`SessionsQueryBody`) instead of a query string, and returns the same response.
Use it when the request parameters must be encrypted. See "Sending parameters
in a POST body" and "Payload encryption" in the API description.
- Request encryption off: send `SessionsQueryBody` as `application/json`.
- Request encryption on: send the same object encrypted as an
`EncryptedRequest`, preferably as `application/jose+json`.
- Response encryption on: every response except `401` is an
`EncryptedResponse` with `X-ABL-Encrypted: true`.
Use this endpoint to retrieve the same session-level analytics as the [corresponding GET operation](/agent-platform/api-reference/analytics-list-session-summaries), but send the query parameters in a JSON request body instead of the URL query string. It supports the same filters, pagination, validation, permissions, and response format as the GET operation.
Use the `POST` endpoint when the request parameters must be encrypted. With request encryption enabled for the Platform Key, send the query object as a JWE encrypted request. With request encryption disabled, send the parameters as a plain JSON object. Response encryption is independent of request encryption; when enabled, responses other than `401` are returned as encrypted JWE payloads.
For filter details, see the [GET endpoint help article](/agent-platform/api-reference/analytics-list-session-summaries).
# List trace events
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-traces
agent-platform/api-specs/traces.yaml get /api/public/analytics/projects/{projectId}/traces
`fromDate` and `toDate` are required. `fromDate` is included in the results and
`toDate` is not, so a full day is
`fromDate=2026-08-05T00:00:00Z&toDate=2026-08-06T00:00:00Z`.
To page through results, pass the `nextCursor` value from the previous response
back as `cursor`. Keep every other parameter the same — a cursor is tied to the
filters, data mode, and sort order it was created with, and changing any of them
makes it invalid.
To fetch one specific event, filter by `eventId`. There is no separate
single-event endpoint.
If you request an event name this API does not support, it is ignored rather
than rejected, and listed back to you in `meta.unsupportedEventNamesRequested`.
Use this endpoint to retrieve the execution events recorded while agents run. The response groups events by trace and can include LLM calls, tool calls, agent decisions, and failures. Each event provides execution metadata and, depending on the selected `dataMode`, either summary information or the complete sanitized request and response content.
Use `dataMode=summary` when you need operational information such as the event type, model, token usage, duration, cost, or error status without retrieving the event payloads. Use `dataMode=full` when you need the detailed sanitized request and response content associated with the events.
The endpoint requires a `fromDate` and `toDate` range. The start of the range is inclusive and the end is exclusive. You can further narrow the results by event type, event name, session, trace, event, agent, decision type, channel, environment, or error status. Results are paginated using a cursor; because pagination counts events before grouping them into traces, a page can contain fewer traces than the requested event limit.
### Filters
* `fromDate` — **Required.** Inclusive start timestamp for the time range.
* `toDate` — **Required.** Exclusive end timestamp for the time range.
* `eventTypes` — Filter by event category:
* `llm` — LLM calls
* `tool` — Tool calls
* `decision` — Agent decisions
* `error` — LLM and tool failures
* `eventNames` — Filter by specific event names:
* `llm.call.completed`
* `llm.call.failed`
* `tool.call.completed`
* `tool.call.failed`
* `tool.call.skipped`
* `agent.decision`
* `sessionIds` — Filter events belonging to specific sessions; up to 10,000 session IDs.
* `eventId` — Return only the event with the specified ID. Use this when you need to retrieve a specific event; there is no separate single-event endpoint.
* `traceId` — Return only events belonging to the specified trace.
* `agentName` — Filter events produced by a specific agent. The value is matched exactly.
* `decisionKind` — Filter agent decision events by decision type, such as `routing`, `handoff`, `escalation`, or `completion`.
* `channel` — Filter by the exact channel name recorded for the event. Unlike the Sessions endpoint, this filter does not support channel aliases.
* `environment` — Filter by environment: `dev`, `staging`, `production`, or `working-copy`.
* `hasError` — Set to `true` to return only events with errors, `false` to return only events without errors, or omit the parameter to return both.
The `eventTypes` filter accepts up to four categories. The `eventNames` filter accepts up to 20 names. Unsupported event names are ignored rather than rejected and are reported in `meta.unsupportedEventNamesRequested`.
Use `GET` when:
* You are retrieving trace data without requiring encrypted request parameters.
* You want to inspect or troubleshoot a specific event using `eventId`.
* You want to investigate a particular trace using `traceId`.
* You are analyzing events for one or more sessions using `sessionIds`.
* You need to query and page through trace data using standard URL parameters.
Do not use GET when the request parameters themselves must be encrypted. GET query strings are never encrypted.
## POST — List trace events with parameters in the request body
Use this endpoint when you need the same trace data and filtering capabilities as the GET endpoint, but want to send the query parameters in the request body instead of the URL. The POST endpoint accepts the same parameters and returns the same response as GET.
The POST endpoint is particularly useful when request parameters must be encrypted. When request encryption is enabled for the Platform Key, send the `TracesQueryBody` as an encrypted JWE. When request encryption is disabled, send the same parameters as a plain JSON object. Response encryption is independent of request encryption; when enabled, responses other than `401` are returned as encrypted JWE payloads.
### Filters and parameters
The POST endpoint supports the same filtering parameters as GET:
* `fromDate` — **Required.** Inclusive start timestamp.
* `toDate` — **Required.** Exclusive end timestamp.
* `eventTypes` — Filter by event category: `llm`, `tool`, `decision`, or `error`.
* `eventNames` — Filter by specific supported event name.
* `sessionIds` — Filter by up to 10,000 session IDs.
* `eventId` — Retrieve a specific event.
* `traceId` — Filter events belonging to a specific trace.
* `agentName` — Filter by the exact agent name.
* `decisionKind` — Filter agent decision events by decision type.
* `channel` — Filter by the exact recorded channel name.
* `environment` — Filter by environment.
* `hasError` — Filter for events that have or do not have errors.
* `dataMode` — Select `summary` or `full` event details.
* `sortOrder` — Sort results in ascending or descending order.
The POST body uses the GET parameter names as JSON property names. List parameters can be sent as JSON arrays or comma-separated strings. `traceDimensions` is not a parameter in this Traces API; do not use it with this endpoint.
Use `POST` method when:
* The request parameters must be encrypted.
* You want to avoid exposing potentially sensitive filter values in the URL.
* You need to send the query as a JSON object rather than URL query parameters.
When request encryption is enabled, the plaintext query object is encrypted as a flattened JWE using the configured Platform Key. A plaintext POST body is rejected when request encryption is enabled, while an encrypted body is rejected when request encryption is disabled.
Do not add query parameters to a POST request. All query parameters must be supplied in the request body, and the body must be a JSON object.
# List trace events using JWE payload
Source: https://koreai.mintlify.app/agent-platform/api-reference/analytics-list-traces-with-encrypted-payload
agent-platform/api-specs/traces.yaml post /api/public/analytics/projects/{projectId}/traces
POST form of `GET` on this path. It accepts the same parameters as a flat JSON
body (`TracesQueryBody`) instead of a query string, and returns the same response.
Use it when the request parameters must be encrypted. See "Sending parameters
in a POST body" and "Payload encryption" in the API description.
- Request encryption off: send `TracesQueryBody` as `application/json`.
- Request encryption on: send the same object encrypted as an
`EncryptedRequest`, preferably as `application/jose+json`.
- Response encryption on: every response except `401` is an
`EncryptedResponse` with `X-ABL-Encrypted: true`.
Use this endpoint when you need the same trace data and filtering capabilities as [the corresponding GET endpoint](/agent-platform/api-reference/analytics-list-traces), but want to send the query parameters in the request body instead of the URL. The `POST` endpoint accepts the same parameters and returns the same response as `GET`.
When request encryption is enabled for the Platform Key, send the `TracesQueryBody` as an encrypted JWE. When request encryption is disabled, send the same parameters as a plain JSON object. Response encryption is independent of request encryption; when enabled, responses other than `401` are returned as encrypted JWE payloads.
The `POST` endpoint supports the same filtering parameters as `GET`. The `POST` body uses the `GET` parameter names as JSON property names. List parameters can be sent as JSON arrays or comma-separated strings. `traceDimensions` is not a parameter in this Traces API; do not use it with this endpoint.
Use `POST` method when:
* You must encrypt the request parameters.
* You want to avoid exposing potentially sensitive filter values in the URL.
* You need to send the query as a JSON object rather than URL query parameters.
When request encryption is enabled, the plaintext query object is encrypted as a flattened JWE using the configured Platform Key. A plaintext `POST` body is rejected when request encryption is enabled, while an encrypted body is rejected when request encryption is disabled.
Do not add query parameters to a `POST` request. All query parameters must be supplied in the request body.
# List survey response
Source: https://koreai.mintlify.app/agent-platform/api-reference/feedback-survey-list
agent-platform/api-specs/public-feedback-survey.yaml get /api/public/feedback/projects/{projectId}
List the project's survey responses
Returns a page of responses with an opaque keyset cursor, with the oldest response first. The cursor is keyset (timestamp, response\_id) rather than an offset, so a client polling forward through history never skips a response or sees one twice as new rows arrive. No per-response score is returned: a score is a property of a population, not of one answer. Requires the `feedback-survey:read` permission.
# Submit rating for survey
Source: https://koreai.mintlify.app/agent-platform/api-reference/feedback-survey-submit-rating
agent-platform/api-specs/public-feedback-survey.yaml post /api/public/feedback/projects/{projectId}
Submit a rating against a named survey
Records one rating for the survey named in the body. The rating is validated against the survey's control range (thumbs 0-1, star 1-5, scale 0-10). The rating is rejected rather than clamped when out of range, so a fabricated value can never reach the score. The response counts toward the survey's headline figure by default, scored by the survey's own method exactly as a runtime-captured response is. It requires the `feedback-survey:write` permission.
# Events of outbound calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-events
agent-platform/api-specs/outbound-calls.yaml get /api/v1/project/{projectSlug}/{env}/outbound-calls/{outboundCallId}/events
Live status stream for one outbound call (server-sent events).
Subscribes to the call's status as a `text/event-stream`. The first event is always a `snapshot`, carrying the call's current `phase`/`outcome` plus its full `statusHistory` to date. Every subsequent event is a `phase` event describing one applied transition. Every event carries a `version` — use it to detect and discard a `phase` event that describes the same transition already included in the initial `snapshot`.
The stream stays open until the call reaches `phase=terminal` (one final event is still delivered) or the client disconnects. There is no guaranteed minimum interval between events; treat gaps as normal, not as evidence of a stall.
# Look-up a call by ID
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-id
agent-platform/api-specs/outbound-calls.yaml get /api/v1/project/{projectSlug}/{env}/outbound-calls/{outboundCallId}
Look-up an outbound call by its ID.
# List outbound calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-listing
agent-platform/api-specs/outbound-calls.yaml get /api/v1/project/{projectSlug}/{env}/outbound-calls
List outbound calls.
Paginated list of outbound calls for the project/environment, most recent first. Destinations are always masked. Combine filters freely; all supplied filters apply together (AND).
# Introduction to outbound dialing API
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-overview
These APIs let an agent make voice calls to customers and manage incoming voice calls. The API supports a proactive customer-engagement requirement:
* An external event or an app triggers a call.
* The voice agent conducts the conversation for answered calls.
* The calling app can monitor and act on the call.
A few specific use cases are:
* Remind user for an upcoming appointment.
* Verify user-placed orders.
* Send delivery notification or seek confirmation.
* Call as a follow-up of a user's contact request.
**Prerequisite** to use this API is an outbound-capable KoreVG [voice channel](/agent-platform/channels#voice-channels).
Pre-configured channels manage the telephony operations, infrastructure, and provider details. It makes the API simple to use by focusing on outcomes such as answered, busy, not answered, or failed calls. After a call is answered, it is handled over to an Artemis agent as a normal agent conversation. It uses idempotency to prevent unnecessary repeat calls.
# Place outbound calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-place
agent-platform/api-specs/outbound-calls.yaml post /api/v1/project/{projectSlug}/{env}/outbound-calls
Initiate an outbound call.
Places an outbound call through the given channel connection. Returns immediately once the call has been accepted for submission — this does not wait for the call to ring or connect.
Returns `201` on a genuinely new call. Repeating the exact same `idempotencyKey` and body returns `200` with the original call instead of placing a duplicate one.
# Search outbound calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-search
agent-platform/api-specs/outbound-calls.yaml post /api/v1/project/{projectSlug}/{env}/outbound-calls/search
Search outbound calls by destination.
Look up calls by the real (unmasked) destination number/SIP URI, without ever putting it in a URL. The destination is matched against a server-side blind index — the raw value in your request is never logged or echoed back; results still return the destination masked, like every other endpoint.
The `destination` is optional — omit it to combine the other filters (phase, outcome, clientReferenceId, channelConnectionId, time window) as a general filtered search.
# Summarize outbound calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-summary
agent-platform/api-specs/outbound-calls.yaml get /api/v1/project/{projectSlug}/{env}/outbound-calls/summary
Aggregate outbound-call KPIs for a time window.
Rolled-up counts for the given filters and time window: total calls, how many wer answered, how many are still in flight, how many hit an answering machine, and a breakdown by terminal outcome.
# Terminate outbound calls
Source: https://koreai.mintlify.app/agent-platform/api-reference/outbound-calls-terminate
agent-platform/api-specs/outbound-calls.yaml post /api/v1/project/{projectSlug}/{env}/outbound-calls/{outboundCallId}/terminate
Terminate or hang up an in-flight outbound call.
Hangs up a call that has not yet reached `phase=terminal`. Drives the call to `phase=terminal`, `outcome=canceled`. Don't use this API on a call that's already terminal. It returns the call's existing terminal state, not an error.
# Artemis API overview
Source: https://koreai.mintlify.app/agent-platform/api-reference/overview
This document covers how to invoke and use the Artemis REST APIs for authentication, request format, error handling, pagination, and more.
## Prerequisites
Before calling any API endpoint, ensure the following are in place:
* An Artemis account with access to the relevant workspace.
* A project created within that workspace.
* Generated API credentials (Platform Key).
* Confirmed scopes and permissions assigned to the credentials for the endpoints that you intend to use.
## Authentication
A platform key is a long-lived bearer credential scoped to a tenant, a project, and a set of scopes. Its format is `abl_<48 hex characters>`, for example `abl_3f7a1c9e42b8dxxxxa1e8c7b4d29f0a3e5c8b1d7f2a4exxxx`. The headers are in the format `X-API-Key: `.
### Create a platform key
Navigation: **Project → Settings → API Keys → Platform Keys → New Key**.
| Field | Value |
| - | - |
| Key Name | Example: `acme-search-integration`. |
| Scopes | Example: `Execute Chat`, `Execute Workflows`. |
| Environments | Choose the environment with which the key is required. |
| Expires | 30 days, 90 days, never, custom date. |
Copy the generated key. It isn't displayed again.
## Scopes and permissions
Scopes determine which resources and actions a given set of credentials can access. Requests made with credentials that lack the required scope for an endpoint fail during authorization, even if authentication succeeds.
Every API request must include the API key in the X-API-Key header. Keys are tied to explicit scopes (for example, execute chat) assigned at creation. Note that access is scope-based, not origin-based.
| Execution scope | Description |
| :- | :- |
| Execute Workflows | Execute workflows via Process API |
| Invoke Draft Workflows | Invoke draft (unpublished) workflow versions via API key. |
| Read Workflows | Read workflow definitions and status |
| Execute Chat | Send messages to agents via Chat API |
| Execute HTTP Async | Initialize HTTP Async sessions and enqueue asynchronous agent messages |
| Read Attachments | Read session attachment metadata, processing status, and download URLs |
| Upload Attachments | Upload session attachments and retry attachment processing |
| Management scope | Description |
| :- | :- |
| Read Agents | List and inspect agent configurations. |
| Write Agents | Create and update agent configurations. |
| Read Channel Connections | Read project channel connection details for service integrations. |
| Write Channel Connections | Create, update, and deactivate/delete project channel connections for service integrations. |
| Read HTTP Async | Read HTTP Async webhook subscriptions and delivery status. |
| Manage HTTP Async | Create, update, and deactivate HTTP Async webhook subscriptions. |
| Read Deployments | List deployment status and history. |
| Write Deployments | Create and promote deployments. |
| Read Solution Workbench | Read published solution contracts, configuration, releases, and analytics. |
| Release Solution Workbench Configurations | Validate and release approved solution configuration versions. |
| Read Sessions | Read session history and transcripts |
| Write Sessions | Attach external provider metadata to sessions (for example, post-transfer contact-center metadata) and trigger transcript sync |
| Close Sessions | Close sessions with an explicit disposition |
| Read Recordings | Read call recording metadata and media URLs |
| Initiate Outbound Calls | Place on-demand outbound voice calls through a configured KoreVG channel |
| Read Outbound Calls | Look up and list outbound call attempts and their retained logs |
| Create Releases | Build immutable CI/CD releases |
| Read Releases | Read CI/CD releases and status |
| Download Release Bundle | Download the immutable release bundle (sensitive) |
| Git Sync | Pull approved branch/tag/commit into a project |
| Git Push | Push Studio changes to a feature/main branch |
| Git Checkout | Switch or create the project source branch |
| Git Force Override | Override loss-prevention on git sync (high privilege) |
| Deploy Release | Deploy a release to an environment |
| Promote Release | Promote a release across environments |
| Rollback | Roll back an environment to a previous release |
| Run Evals | Run eval suites as CI/CD gates |
| Read Deployment Health | Read deployment/runtime health checks. |
| Read Audit Evidence | Read CI/CD audit evidence packs. |
| Manage CI/CD Webhooks | Manage deployment-event webhook subscriptions. |
| Bypass Gate | Bypass qualification gate (high privilege, audited). |
| Force Lock Release | Force-release a stale deployment lock (high privilege). |
| Knowledge base scope | Description |
| :- | :- |
| Query Knowledge Base | Execute search queries against knowledge bases. |
| Read Knowledge Base | Read knowledge base metadata and configurations. |
| Ingest Documents | Upload files and ingest data into knowledge bases. |
| Write Document Permissions | Set document-level ACL permissions during ingestion. |
| Read Field Intelligence | Read Field Intelligence field summaries and details for project knowledge bases. |
| Preview Field Intelligence | Preview Field Intelligence changes without saving those. |
| Write Field Intelligence | Create and update Field Intelligence configuration. |
| Refresh Field Intelligence | Request Field Intelligence projection refresh jobs. |
| Import Field Intelligence | Import source-derived Field Intelligence configuration. |
| Analytics scope | Description |
| :- | :- |
| Read Analytics | Read analytics dashboards, metrics, and saved widget data. |
| Administration scope | Description |
| :- | :- |
| Read Workspace | Read workspace settings and usage data. |
## Base URL
All API requests target your platform instance base URL of the format `https://example.com/api/public`. For URL of your instance, see the list of [region-specific platform URLs](/agent-platform/getting-started#region-specific-urls-and-allowlist-of-ips).
## Common request parameters
| Element | Description | Example |
| - | - | - |
| X-API-Key header | Carries the platform key used to authenticate the request. | X-API-Key: `abl_xxxxx` |
| Content-Type header | Specifies the payload format. | `application/json` |
| Request body | Endpoint-specific payload. | See individual endpoint reference. |
| projectSlug | Unique identifier for the project | Project > Settings > Advanced > slug. |
| agentSlug | Unique identifier for the agent | Agent > Settings > slug |
## Pagination
List endpoints use **offset-based pagination**:
| Parameter | Type | Default | Description |
| - | - | - | - |
| limit | integer | 50 | Items per page (maximum 200) |
| offset | integer | 0 | Items to skip |
Example
```json theme={null}
# Fetch page 1
GET /api/projects/proj_abc/sessions?limit=50&offset=0
# Fetch page 2
GET /api/projects/proj_abc/sessions?limit=50&offset=50
```
Paginated responses include a pagination object.
```json theme={null}
{
"success": true,
"sessions": [...],
"pagination": {
"total": 142,
"limit": 50,
"offset": 0
}
}
```
## HTTP error codes
| Error code | HTTP code | Potential cause | Fix |
| - | - | - | - |
| `BAD_REQUEST` | 400 | Invalid or missing fields | Check request body schema |
| `VALIDATION_ERROR` | 400 | Field value type mismatch | Review field types and ranges |
| `UNAUTHORIZED` | 401 | Not authenticated | Verify key is valid and not expired |
| `FORBIDDEN` | 403 | Insufficient permissions | Check credential scope |
| `NOT_FOUND` | 404 | Resource missing or unavailable | Verify resource ID and access rights |
| `RATE_LIMIT_EXCEEDED` | 429 | Request rate limit hit | Retry after `Retry-After` duration (seconds) |
| `QUEUE_FULL` | 429 | Execution queue at capacity | Reduce concurrency, retry after `retryAfterMs` |
| `INTERNAL_ERROR` | 500 | Unexpected server error | Retry after a brief delay |
| `SERVICE_UNAVAILABLE` | 503 | Backend dependency offline | Retry later |
# Get project import status
Source: https://koreai.mintlify.app/agent-platform/api-reference/project-export-get
agent-platform/api-specs/project-import-export.yaml get /v1/projects/{projectId}/export/{exportId}
This endpoint gets the export status of a project.
# Start project export
Source: https://koreai.mintlify.app/agent-platform/api-reference/project-export-post
agent-platform/api-specs/project-import-export.yaml post /v1/projects/{projectId}/export
Export a project.
This endpoint starts an asynchronous export. The server checks project readiness, discovers agent/tool dependencies, includes required dependency layers, assembles the archive, and generates its manifest and lockfile.
# Get project import status
Source: https://koreai.mintlify.app/agent-platform/api-reference/project-import-get
agent-platform/api-specs/project-import-export.yaml get /v1/projects/{projectId}/import/{importId}
This endpoint gets the import status of a project.
# Start project import
Source: https://koreai.mintlify.app/agent-platform/api-reference/project-import-post
agent-platform/api-specs/project-import-export.yaml post /v1/projects/{projectId}/import
Import a project.
This endpoint starts an asynchronous import. The server runs the Builder-equivalent preview and dependency validation before applying any mutation. Blocking validation errors prevent staging and activation.
# Get all recordings for a session
Source: https://koreai.mintlify.app/agent-platform/api-reference/recording-fetch-calls-per-session
agent-platform/api-specs/call-recordings.yaml get /api/public/recordings/projects/{projectId}/sessions/{sessionId}/recordings
Fetch a session's call recordings
The Call Recording API provides read-only access to server-side call recordings associated with a conversation session. It returns recording metadata and media URLs for available audio files, supporting automated workflows such as compliance archiving, quality assurance, and analytics.
A session can produce multiple recordings, such as when a call is transferred. Each recording can contain multiple files, such as separate caller and agent channels. The API does not return transcripts or other conversation text.
Authenticate using a project-bound platform API key (`abl_...`) with the `recordings.read` scope. Send the key in either the `x-api-key` header or the `Authorization: Bearer` header. Studio user JWTs, JWT App assertions, and public `pk_` keys are not accepted.
## Retrieve session recordings
Use this endpoint to retrieve all recordings produced by a specific session. The response includes recording metadata and a list of files for each recording. Each file includes its channel, file name, size, recording timestamps, availability, and media URL details.
The endpoint requires the project ID and session ID. The optional `disposition` query parameter controls how the media is delivered:
* `attachment` (default): downloads the media file.
* `inline`: allows the media to play in a browser.
## Response status and availability
An HTTP `200` response can include recordings or explain why recording media is not yet available. Check `data.status`, `data.retryable`, and the availability fields on individual recordings and files.
| `data.status` | Meaning |
| - | - |
| `success` | Every file has a media URL. |
| `partial` | At least one file has a URL and at least one does not. |
| `call_in_progress` | No file has a URL, and the call is still in progress. |
| `media_in_progress` | No file has a URL, and media is still being fetched or stored. |
| `media_unavailable` | No file is available, and no media is pending. |
When `data.recordings` is empty, use `data.unavailableReason` and `data.retryable` to understand why no recordings are available.
A file without a URL includes an `unavailableReason` and a `retryable` flag. Retry with backoff while the relevant `retryable` value is `true`. Stop retrying when it is `false`.
## Media URLs
Use `urlKind` to determine how to retrieve each file:
* `signed`: Send a `GET` request directly to the absolute URL without an authentication header.
* `proxied`: Prefix the returned relative URL with the API host and send a `GET` request to it without an authentication header. Use the URL as returned; do not construct it manually.
* `external`: Send a `GET` request directly to the customer-owned absolute URL.
Media URLs are short-lived where the platform controls their expiry. The default lifetime is 900 seconds, with a maximum of 3600 seconds. The `expiresIn` field indicates the remaining lifetime in seconds when applicable; it is `null` for external URLs.
Treat every media URL as a bearer secret. Anyone with access to the URL may be able to retrieve the recording.
## Errors and limits
* `400`: Invalid session ID or invalid `disposition`.
* `401`: Missing, invalid, expired, or revoked credentials.
* `403`: Missing permission, unsupported credential type, or call recording not enabled for the tenant.
* `404`: Project or session not found within the API key's authorized scope.
* `413`: Recording or stored-file limit exceeded.
* `429`: Tenant request rate limit exceeded.
* `500`: Unexpected failure.
* `503`: Project recording settings could not be resolved; retry later.
The API allows up to 64 recordings per session and, by default, up to 32 stored files across those recordings. Tenant-level request rate limits also apply.
## Audit and data handling
When at least one media URL is issued, the API records a `recording:public_media_urls_issued` audit event attributed to the calling API key. Responses include `Cache-Control: no-store`, `Pragma: no-cache`, and `Referrer-Policy: no-referrer`.
# Stream a recording
Source: https://koreai.mintlify.app/agent-platform/api-reference/recording-stream
agent-platform/api-specs/call-recordings.yaml get /api/projects/{projectId}/recordings/{recordingId}/media/content/{mediaToken}
Stream a recording.
The Call Recording API provides read-only access to server-side call recordings associated with a conversation session. It returns recording metadata and media URLs for available audio files, supporting automated workflows such as compliance archiving, quality assurance, and analytics.
A session can produce multiple recordings, such as when a call is transferred. Each recording can contain multiple files, such as separate caller and agent channels. The API does not return transcripts or other conversation text.
Authenticate when retrieving session recordings using a project-bound platform API key (`abl_...`) with the `recordings.read` scope. Send the key in either the `x-api-key` header or the `Authorization: Bearer` header. Studio user JWTs, JWT App assertions, and public `pk_` keys are not accepted.
## Stream recording media
Use this endpoint to stream a recording when the session recordings endpoint returns a file with `urlKind: proxied`. Prefix the returned relative URL with the API host and send a `GET` request to the complete URL. Don't construct the URL on your own.
The `mediaToken` embedded in the URL is the credential for this request. **Do not send an `x-api-key` or `Authorization` header.** The token is short-lived and bound to the tenant, project, recording, file index, disposition, and issuing API key.
This endpoint supports HTTP range requests for seeking or retrieving part of a file. Send a `Range` header, such as `Range: bytes=0-65535`, to request a byte range.
## Response behavior
* `200 OK`: Returns the full media file.
* `206 Partial Content`: Returns the requested byte range.
* `302 Found`: The file is hosted at an external URL; follow the redirect.
## Errors
* `400`: Invalid file index or media disposition.
* `401`: The media token is invalid, expired, or does not match the request.
* `404`: The recording or requested file is not found, or the stored media object is missing.
* `409`: The recording is not yet complete (`RECORDING_NOT_READY`).
* `416`: The requested byte range cannot be satisfied.
* `504`: Streaming did not finish within five minutes.
# Execute a specific tool
Source: https://koreai.mintlify.app/agent-platform/api-reference/tools-execute-table-tools-execute
agent-platform/api-specs/tool-execution.yaml post /api/v1/project/{projectSlug}/tools/{toolName}/invoke
Executes a tool in a project and returns its result. The request supplies the tool's caller-defined input parameters in a JSON body. Execution is synchronous: the request waits for the tool to complete or reach its timeout.
Use this endpoint to run an executable tool after identifying it and retrieving its input schema. The tool's output is returned without being reshaped by the platform, except that an Agent Table tool's output is returned as a JSON object.
**Permission:** `tool:execute`, granted by the platform API key scope `tools.execute`.
## Path parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `projectSlug` | string | Yes | Project slug, not the project ID or UUID. |
| `toolName` | string | Yes | Tool name as authored. For MCP tools, use the `alias__toolName` format. |
## Request body
Send a JSON object with the following properties:
| Property | Type | Required | Description |
| - | - | - | - |
| `params` | object | Yes | Tool input matching the `input` schema returned by the Describe a tool endpoint. |
| `timeout` | integer | No | Per-call timeout in milliseconds. Minimum: `1000`; maximum: `300000`. |
| `mode` | string | No | Execution mode. Omit this property or set it to `sync`. Other values return `400 ASYNC_NOT_SUPPORTED`. |
The request body is strict. Unknown top-level properties and platform-derived fields are rejected rather than ignored. Platform-derived fields include values such as `tenantId`, `environment`, and `agentSession`.
For Agent Table tools without a query template, `params` contains a structured call with `op` and the operands for that operation. The accepted operations depend on the tool's binding.
## Authentication
Use one of the supported credentials:
* `x-api-key: abl_...` — platform API key.
* `Authorization: Bearer ` — platform API key or Studio user JWT.
* `auth: ` — JWT App assertion.
Public SDK keys (`pk_...`) are not accepted. The JWT App assertion must be sent in the `auth` header, not the `Authorization` header.
The credential must have the `tool:execute` permission. A sensitive-column lookup also requires `table:sensitive:reveal`.
## Response
A successful response contains `success` and `data`. The `data` object includes the execution `status`, the tool's `output` when provided, and `trace` information when the caller is entitled to it.
The response headers identify the tool type, resolved environment, and configuration source:
| Header | Description |
| - | - |
| `X-ABL-Tool-Type` | Type of tool that ran. |
| `X-ABL-Environment` | Environment against which the tool definition was resolved. Always `draft` on this API surface. |
| `X-ABL-Config-Source` | Indicates whether the configuration came from a pinned version or the working copy. |
For Agent Table tools, `output` is a JSON object rather than a serialized JSON string. For other tool types, the output is returned without platform-level reshaping.
## Execution considerations
* Only tool types enabled for public API execution can be invoked. Agent Table tools are enabled by default; other supported types require explicit enablement.
* The `sandbox` tool type is not executable through this API.
* A tool's declared effect can indicate whether it is read-only, writes state, or has side effects. Review this metadata before invoking the tool.
* Tool failures are returned using status codes appropriate to the failure. Do not assume every execution failure is a transient server error or safe to retry.
## Errors
| HTTP status | Description |
| - | - |
| `400` | Malformed or invalid request, unknown key, unsupported platform field, unsupported execution mode, or forbidden operation. |
| `401` | Missing or invalid credentials, or an unsupported public SDK key. |
| `403` | Missing execution permission, insufficient project scope, or a sensitive-column lookup without the required permission. |
| `404` | Project or tool not found, tool type disabled, tool not executable, or public execution disabled. Some of these cases intentionally return the same response. |
| `409` | Conflict requiring caller action, such as a unique-constraint violation, row-version conflict, or unsupported authentication-profile mode. |
| `413` | Request payload exceeds the allowed size or structural limits. |
| `415` | Unsupported media type or charset. A JWT App configured to require payload encryption must use the required encrypted format. |
| `422` | Tool configuration or execution failure, invalid tool response, or SOAP fault. |
| `429` | Tenant request or tool-call rate limit exceeded. |
| `500` | Unexpected execution failure. |
| `502` | Tool network, HTTP, authentication, or upstream failure. |
| `503` | A dependency is unavailable, such as an MCP server or Agent Table service. |
| `504` | The tool did not respond within its timeout. |
The `429` response uses a rate-limiter-specific response format rather than the standard API error envelope. Check the response headers for rate-limit information and retry guidance.
For transient dependency failures (`503`), retry with backoff. Resolve conflicts such as `409` before retrying. Do not automatically retry operations with side effects unless their retry behavior is known to be safe.
# List the available project tools
Source: https://koreai.mintlify.app/agent-platform/api-reference/tools-execute-table-tools-list
agent-platform/api-specs/tool-execution.yaml get /api/v1/project/{projectSlug}/tools
Lists the tools available for execution in a project. Each result provides a tool summary, including its name, type, description, declared effect, and execution availability. The endpoint returns only tools whose types are enabled for public API execution.
Use this endpoint to discover available tools before retrieving a tool's input schema or invoking it. Results are paginated using a cursor based on the tool name.
**Permission:** `tool:read`, granted by the platform API key scope `tools.read`.
## Path parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `projectSlug` | string | Yes | Project slug, not the project ID or UUID. |
## Query parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `limit` | integer | No | Number of tools to return per page. Defaults to `50`, with a minimum of `1` and a maximum of `200`. The value is clamped to the configured maximum. |
| `cursor` | string | No | Opaque cursor returned in `nextCursor` by the previous response. Omit it to retrieve the first page. |
| `type` | array of strings | No | Restricts results to specified tool types. Repeat the parameter to specify multiple types. |
The `table` tool is supported. The API rejects the other tool types.
## Authentication
Use one of the supported credentials:
* `x-api-key: abl_...` — platform API key.
* `Authorization: Bearer ` — platform API key or Studio user JWT.
* `auth: ` — JWT App assertion.
Public SDK keys (`pk_...`) are not accepted. The JWT App assertion must be sent in the `auth` header, not the `Authorization` header.
## Response
A successful response contains `success` and `data`. The `data` object includes:
| Property | Description |
| - | - |
| `environment` | Environment against which tool definitions were resolved. Always `draft` on this API surface. |
| `tools` | Array of tool summaries. |
| `nextCursor` | Cursor for the next page, or `null` when no more results are available. |
Each tool summary can include `name`, `type`, `description`, `effect`, `requiresAuthProfile`, `operations`, `executable`, and `unsupportedReason`, depending on the tool.
The `executable` property indicates whether a tool can be invoked through this API. The `unsupportedReason` property, when present, explains why it cannot be executed.
To retrieve the input schema for a tool, use the **Describe a tool** endpoint.
## Errors
| HTTP status | Description |
| - | - |
| `400` | Invalid request, including an invalid tool type filter. |
| `401` | Missing or invalid credentials, or an unsupported public SDK key. |
| `403` | The credential lacks the required permission or project access. |
| `404` | Project or resource not found. |
| `429` | Tenant request or tool-call rate limit exceeded. |
The `429` response uses a rate-limiter-specific response format rather than the standard API error envelope. Check the response headers for rate-limit information and retry guidance.
# Describe a tool and its input schema
Source: https://koreai.mintlify.app/agent-platform/api-reference/tools-execute-table-tools-retrieve
agent-platform/api-specs/tool-execution.yaml get /api/v1/project/{projectSlug}/tools/{toolName}
This API retrieves a tool's summary and its input schema as JSON Schema. The schema describes the parameters that callers may supply when invoking the tool.
Use this endpoint after discovering a tool to determine its accepted inputs and construct a valid invocation request. Parameters resolved by the platform at runtime, such as configuration values, authentication-profile values, expression results, lookup results, and other server-side values, are omitted from the published schema.
Permission requirement `tool:read`, granted by the platform API key scope `tools.read`.
## Path parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `projectSlug` | string | Yes | Project slug, not the project ID or UUID. |
| `toolName` | string | Yes | Tool name as authored. For MCP tools, use the `alias__toolName` format. |
This endpoint does not define query parameters or a request body.
## Authentication
Use one of the supported credentials:
* `x-api-key: abl_...` — platform API key.
* `Authorization: Bearer ` — platform API key or Studio user JWT.
* `auth: ` — JWT App assertion.
Public SDK keys (`pk_...`) are not accepted. The JWT App assertion must be sent in the `auth` header, not the `Authorization` header.
## Response
A successful response contains `success` and `data`. The `data` object includes the tool's summary properties and an `input` property containing the JSON Schema for caller-supplied parameters.
The response can include the following properties:
| Property | Description |
| - | - |
| `name` | Tool name. |
| `type` | Tool type. |
| `description` | Tool description, when defined. |
| `effect` | Declared effect of the tool, when defined. |
| `requiresAuthProfile` | Indicates whether the tool resolves an authentication profile server-side. |
| `operations` | Operations allowed by the binding for an Agent Table tool. |
| `executable` | Indicates whether the tool can be invoked through this API. |
| `unsupportedReason` | Reason the tool cannot be executed, when applicable. |
| `input` | JSON Schema describing the parameters the caller may supply. |
## Input schema behavior
The `input` schema includes only caller-suppliable parameters. Parameters resolved by the platform are omitted rather than published as caller inputs.
For an Agent Table tool without a query template, the schema describes the structured call accepted by the executor. This includes `op` and the operands applicable to the selected operation. Only `op` is unconditionally required; the applicability of other operands depends on the selected operation.
A tool whose definition cannot be parsed can still be returned. In that case, `executable` is `false`, and `unsupportedReason` is `TOOL_DEFINITION_INVALID`.
Use the returned schema to construct the `params` object for the **Execute a tool** endpoint.
## Errors
| HTTP status | Description |
| - | - |
| `401` | Missing or invalid credentials, or an unsupported public SDK key. |
| `403` | The credential lacks the required permission or project access. |
| `404` | Project or tool not found, or the resource is unavailable. |
| `429` | Tenant request or tool-call rate limit exceeded. |
The `429` response uses a rate-limiter-specific response format rather than the standard API error envelope. Check the response headers for rate-limit information and retry guidance.
# Arch AI
Source: https://koreai.mintlify.app/agent-platform/arch-ai
Arch™ AI is a multi-agent system that drives every phase of the agent lifecycle — from initial design to continuous production optimization. It is not a copilot or a suggestion engine. Arch AI interviews stakeholders, proposes agent topology, generates ABL™, runs tests, monitors production traces, and surfaces improvement proposals — each phase end-to-end, with human review at the decision points.
Arch connects directly to the ABL compiler and the platform runtime. It exchanges compiled IR, traces, and topology data through MCP, so it operates on the same artifacts the platform executes.
## Lifecycle phases
Arch operates across six phases. Each phase is driven by a specialized sub-agent. There is no separate manual step to author ABL, run tests, or promote between environments.
| Phase | What Arch Does |
| - | - |
| **Design** | Interprets user intent and generates a complete agent blueprint — goals, flows, tools, memory, and guardrails — ready to compile. |
| **Build** | Authors ABL, validates cross-references, resolves tool bindings, and confirms deployment readiness before a human reviews. |
| **Test** | Runs Persona × Scenario × Evaluator matrices autonomously, surfacing failures, edge cases, and policy violations without manual test authoring. |
| **Deploy** | Manages environment promotion, versioning, and rollout. Compiler validation ensures no broken agent reaches production. |
| **Observe** | Monitors trace events, session health, and runtime anomalies in real time, surfacing what matters rather than raw logs. |
| **Optimize** | Continuously analyzes performance, cost, and quality signals. Proposes ABL patches as pull requests. Engineers review and approve every change. |
## The authoring flow
The authoring flow runs in stages, from requirements to a deployed, diffable agent artifact. Human review happens at two points: after Blueprint and before Optimize applies changes.
```mermaid actions={false} theme={null}
%%{init: {'theme': 'base', 'themeVariables': {'background': 'transparent', 'primaryColor': '#EAF8E4', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#5CC83A', 'lineColor': '#5CC83A', 'secondaryColor': 'transparent', 'tertiaryColor': 'transparent', 'clusterBkg': 'transparent', 'clusterBorder': '#5CC83A', 'titleColor': '#1a1a1a', 'clusterLabelBackground': 'transparent', 'edgeLabelBackground': '#EAF8E4'}}}%%
flowchart LR
A[Interview] --> B[Blueprint]
B --> C{Human Review}
C -- Accept --> D[Build]
C -- Request Changes --> B
```
```mermaid actions={false} theme={null}
%%{init: {'theme': 'base', 'themeVariables': {'background': 'transparent', 'primaryColor': '#EAF8E4', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#5CC83A', 'lineColor': '#5CC83A', 'secondaryColor': 'transparent', 'tertiaryColor': 'transparent', 'clusterBkg': 'transparent', 'clusterBorder': '#5CC83A', 'titleColor': '#1a1a1a', 'clusterLabelBackground': 'transparent', 'edgeLabelBackground': '#EAF8E4'}}}%%
flowchart LR
D[Build] --> E[Test]
E --> F[Deploy]
F --> G[Observe]
G --> H[Optimize]
H --> D
```
***
### Design and Blueprint
Arch begins by capturing requirements through a structured conversation — accepting a natural-language brief, an SOP document, or a prompt. Stakeholders are the source of truth. Arch produces structured requirements and then proposes a full agent topology: which specialists to create, how handoffs are routed, which tools each agent binds to, and what guardrails apply.
You can review and refine the blueprint before any ABL is generated.
*Blueprint artifact with agent details:*
*Agent topology proposed by Arch:*
### Build
The build sub-agent generates the ABL artifact from the approved blueprint. The ABL compiler validates contracts and references on every generation step. Tests are auto-generated. The output is a deployable, diffable, code-reviewable agent — not a sketch or a configuration dialog.
*ABL definitions generated by Arch:*
### Observe and Optimize
After deployment, Arch's observability sub-agent reads typed trace events, surfaces session health metrics, and flags anomalies — across every agent in the project. When it detects a degradation, a gap in coverage, or a routing inefficiency, it proposes ABL changes as pull requests. The compiler validates the shape of each proposal before engineers review the diff.
The optimization loop closes itself: production signal feeds back into ABL improvements, which compile, deploy, and generate new signal. See [Optimize with Arch](/agent-platform/optimize).
*Project health monitoring and continuous improvement by Arch:*
## Arch as a multi-agent system
Arch is itself built on the same multi-agent architecture it creates. Each specialist sub-agent has a typed role and scoped responsibility.
| Specialist | Role |
| - | - |
| **Multi-agent architect** | Designs agent topology and handoff routing. |
| **ABL construct expert** | Writes valid ABL grammar, types, and contracts. |
| **Channel and voice expert** | Handles channel-specific authoring requirements for voice, web, and messaging. |
| **Integration methodologist** | Designs tool connectors and data source bindings. |
| **Testing and eval expert** | Generates regression suites and evaluation matrices. |
| **Diagnostician and analyst** | Monitors production traces and drives optimization proposals. |
Each specialist operates with the same discipline applied to every customer agent: typed inputs, scoped memory, explicit handoffs, and compiler-validated outputs.
## Accessing Arch
Arch is available from three surfaces. All three use the same MCP interface and produce the same compiled output.
| Surface | How It Works |
| - | - |
| **Agent Studio** | The built-in Arch assistant in Studio. Available throughout the authoring, test, and deployment workflow. Accessible from the sidebar of any agent project. |
| **External coding tools** | Claude Code, Cursor, and Codex connect to Arch through MCP. Agents can be designed and iterated from a CLI or IDE without opening Studio. |
| **Platform runtime** | Arch reads live trace data from the runtime directly. The observability and optimize phases do not require a separate session — Arch works continuously in the background. |
# Channels
Source: https://koreai.mintlify.app/agent-platform/channels
Channels connect your deployed agents to the surfaces where users interact: your own apps, voice calls, messaging apps, webhooks, and other agent frameworks. Artemis provides channel adapters that handle the protocol-specific details, so a single agent definition works across every channel without modification.
A channel always binds to a deployment in a specific environment. Configure deployments first, then add channels on top.
Navigation: **Project** > **Deployments** > **Channels**
| Channels Category | Used by | How It Works |
| - | - | - |
| [Messaging or Digital](#messaging-or-digital-channels) | Slack, WhatsApp, Teams, and other messaging apps | You register the external provider's credentials. The platform returns a webhook URL that the provider calls when a user sends a message. |
| [Voice](#voice-channels) | Realtime LLM Voice, Pipeline Voice, VXML IVR, AudioCodes | A channel connection registers the voice provider's credentials. The platform returns a SIP endpoint or webhook the provider calls to route audio. |
| [SDK](#sdk-channels) | Web SDK, API, Mobile SDK | A public API key (`pk_`) or REST API call bootstraps a session, either embedded in your app or called server-to-server. |
| [Webhook](#webhook-channel) | Webhooks (HTTP Async) | Agent responses are pushed to a webhook URL on your own server. |
| [Protocols](#protocols) | AG-UI (CopilotKit), Agent-to-Agent (A2A) | The agent speaks an open protocol directly, so it plugs into another framework or frontend without a custom adapter. |
All five route to the same agent definition. The adapter normalizes inbound and outbound formats, so your agent doesn't need channel-specific logic for basic conversations.
***
## Messaging or digital channels
The Platform supports the following messaging or digital channels:
| Channel | Description |
| - | - |
| [Slack](/agent-platform/channels/slack) | Deploy agents as Slack bots for team collaboration |
| [LINE](/agent-platform/channels/line) | Connect agents to LINE Official Accounts for chat and media messaging |
| [Microsoft Teams](/agent-platform/channels/ms-teams-copilot) | Integrate agents into Teams channels and chats |
| [WhatsApp](/agent-platform/channels/whatsapp) | Connect agents to WhatsApp Business for customer messaging |
| [Messenger](/agent-platform/channels/messenger) | Connect agents to Facebook Messenger for customer support |
| [Twilio SMS](/agent-platform/channels/twilio) | Send and receive SMS messages via Twilio for customer engagement |
| [Telegram](/agent-platform/channels/telegram) | Connect agents to Telegram bots for messaging and group conversations |
| [Zendesk](/agent-platform/channels/zendesk) | Connect agents to Zendesk Sunshine Conversations for customer support |
| [Instagram](/agent-platform/channels/instagram) | Connect agents to Instagram Messaging for direct customer conversations |
| [Genesys](/agent-platform/channels/genesys) | Connect agents to Genesys Cloud as a Bot Connector for contact center automation |
| [Genesys Open Messaging](/agent-platform/channels/genesys-open-messaging) | Connect Genesys Cloud Open Messaging conversations through a project-scoped webhook adapter |
| Email | Process inbound emails and send agent responses via email |
| [AI for Work](/agent-platform/channels/ai-for-work) | Kore.ai's Employee Experience (EX) platform — bidirectional agent messaging integration |
| [Kore Agent Assist](/agent-platform/channels/kore-agent-assist) | Kore.ai Agent Assist — real-time agent guidance. The platform generates the API key; the external endpoint contract is unchanged. |
Each setup page covers credential registration, webhook configuration, channel-specific features (such as Slack threading and slash commands, or WhatsApp interactive messages), and troubleshooting.
***
## Voice channels
The Platform supports the following voice channels:
| Channel | Description |
| - | - |
| [Realtime LLM Voice](/agent-platform/channels/voice#set-up-realtime-llm-voice-channel) | Live voice conversations using realtime LLM models (OpenAI Realtime, Gemini Live) via Kore.ai Voice Gateway |
| [Pipeline Voice](/agent-platform/channels/voice#set-up-pipeline-voice-channel) | Traditional STT → LLM → TTS voice pipeline via Kore.ai Voice Gateway, with configurable ASR/TTS models |
| VXML IVR | VoiceXML 2.1 gateway for traditional IVR systems, with DTMF and prompt-based navigation |
| [Genesys Audio Connector](/agent-platform/channels/genesys-audio-connector) | Configure Genesys Audio Connector for KoreVG-owned AudioHook execution |
| AudioCodes | Connect voice calls via AudioCodes VoiceAI Connect |
Each voice setup page covers caller identity fields (`session.anonymousId` and related), voice-specific `VOICE:` response blocks, SSML control, and troubleshooting.
***
## SDK channels
The Platform supports the following SDK channels to embed an agent in your own application or call it server-to-server:
| Channel | Description |
| - | - |
| [Web SDK](/agent-platform/channels/web-sdk-channel) | Embed a chat widget in your website with a single script tag |
| API | Programmatic access via REST API for server-to-server integration |
| Mobile SDK | Native iOS and Android SDKs for in-app agent experiences |
***
## Webhook channel
The Platform supports the following webhook channel to receive asynchronous events:
| Channel | Description |
| - | - |
| [Webhooks (HTTP Async)](/agent-platform/channels/webhook) | Receive agent responses via webhook callbacks to your server |
***
## Protocols
The Platform supports the following open protocols to connect agents with other agent frameworks and frontends:
| Protocol | Description |
| - | - |
| [AG-UI (CopilotKit)](/agent-platform/channels/ag-ui-copilotkit) | Server-sent events protocol for React/Next.js frontend agent UIs, with streaming support |
| [Agent-to-Agent (A2A)](/agent-platform/channels/agent-to-agent) | Google A2A protocol for inter-agent communication with task lifecycle management |
***
**Related articles:**
* [Deploy Agents](/agent-platform/deployment)
# Set up AG-UI (CopilotKit)
Source: https://koreai.mintlify.app/agent-platform/channels/ag-ui-copilotkit
[Back to protocols](/agent-platform/channels#protocols)
Supports bi-directional protocol connections for React frontend agent UIs built with CopilotKit.
To configure an AG-UI (CopilotKit) channel, go to **Deployments** > **Channels** > **Protocols** > **AG-UI (CopilotKit)** > **+ New** and complete the following steps in order.
On the **New AG-UI (CopilotKit) Connection** page, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection. Up to 255 characters. |
| **Frontend App Name** | The name of the frontend application connecting to this channel, for example `copilot-dashboard`. Up to 255 characters. |
| **Environment** | The deployment the connection binds to. Select an environment to follow its active deployment automatically, or leave the default to use the working copy. |
| **Provider Verification Strength** | Controls how strictly the platform trusts the provider's identity assertions for session continuity and linking. **Weak** is the default. Choose **Strong** only for channels/providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. |
Click **Create**.
### Manage an existing AG-UI (CopilotKit) channel
Open a channel from **Deployments** > **Channels** > **AG-UI (CopilotKit)** shows three tabs: **Overview**, **Configuration**, and **Deployment**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | for example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | for example: `dev` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique channel identifier (UUID) |
**Setup Instructions** shows the **Frontend App Name** configured for this connection, with a copy icon to copy it into your integration code.
**General**
| Field | Description |
| - | - |
| **Display Name** | The connection's label. Editable at any time. |
| **Provider Verification Strength** | Same setting described in [Set up AG-UI (CopilotKit)](#set-up-ag-ui-copilotkit) above. |
**AG-UI (CopilotKit) Settings**
Protocol channels are configured through the SDK integration — no additional settings are needed here.
Click **Save Changes** to apply changes.
The **Deployment** tab shows these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Development`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and when it was deployed. |
Click **Save Changes** after switching the environment/version.
# Set up Agent-to-Agent (A2A)
Source: https://koreai.mintlify.app/agent-platform/channels/agent-to-agent
[Back to protocols](/agent-platform/channels#protocols)
Supports the Google A2A protocol for inter-agent communication with task lifecycle management.
To configure an Agent-to-Agent (A2A) channel, go to **Deployments** > **Channels** > **Agent-to-Agent (A2A)** > **+ New** and complete the following steps in order.
On the **New Agent-to-Agent (A2A) Connection** page, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection. Up to 255 characters. |
| **Agent Endpoint Name** | A system name for the A2A endpoint, for example `booking-agent-a2a`. Up to 255 characters. |
| **Environment** | The deployment the connection binds to. Select an environment to follow its active deployment automatically, or leave the default to use the working copy. |
| **Provider Verification Strength** | Controls how strictly the platform trusts the provider's identity assertions for session continuity and linking. **Weak** is the default. Choose **Strong** only for channels/providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. |
Click **Create**.
### Manage an existing Agent-to-Agent (A2A) channel
Open a channel from **Deployments** > **Channels** > **Agent-to-Agent (A2A)** shows three tabs: **Overview**, **Configuration**, and **Deployment**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | for example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | for example: `dev` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique channel identifier (UUID) |
**Setup Instructions** shows the **Agent Endpoint Name** configured for this connection, with a copy icon to copy it into your integration code.
**General**
| Field | Description |
| - | - |
| **Display Name** | The connection's label. Editable at any time. |
| **Provider Verification Strength** | Same setting described in [Set up Agent-to-Agent (A2A)](#set-up-agent-to-agent-a2a) above. |
**Agent-to-Agent (A2A) Settings**
Protocol channels are configured through the SDK integration — no additional settings are needed here.
Click **Save Changes** to apply changes.
The **Deployment** tab shows these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Development`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and when it was deployed. |
Click **Save Changes** after switching the environment/version.
# Set up AI for Work
Source: https://koreai.mintlify.app/agent-platform/channels/ai-for-work
[Back to channels](/agent-platform/channels)
Connect your agent to Kore.ai AI for Work, the Employee Experience (EX) platform, as a bidirectional messaging integration. Employees reach the agent through a published Autonomous Agent in AI for Work, and AI for Work reaches the agent through an endpoint URL and connection secret that you generate in Artemis.
Artemis labels this channel **AIforWork**. The product it connects to is documented as AI for Work. This article uses **AIforWork** for the on-screen label and AI for Work for the product.
## Before you begin
You need administrator access to the AI for Work Admin Console and permission to create and publish Autonomous Agents in the target workspace. You also need a project in Artemis with at least one deployed version in the environment you plan to bind the connection to.
Create the connection in Artemis first. The endpoint URL and connection secret it generates are the inputs to the AI for Work side of the setup.
## Create the channel connection in Artemis
Open Artemis, select the required project, and go to **Deployments** -> **Channels** -> **AIforWork** -> **+ New**. On the **New AIforWork Connection** dialog, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection, for example `AIforWork Production`. Up to 255 characters. |
| **Environment** | Select an environment to follow its active deployment. If no environment has an active deployment yet, leave the default and bind the connection later from the **Deployment** tab. |
| **Provider Verification Strength** | **Weak (default)** or **Strong**. Choose **Strong** only for channels or providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. Set this to **Active**. |
Select **Create**.
The **AIforWork Connection Created** dialog shows the **Endpoint URL** and the **Connection Secret**. Copy both values and store the secret securely, then select **Done**.
The connection secret appears only once and can't be retrieved afterward. If you lose it, open the connection and select **Rotate** on the **Overview** tab to issue a new one. Rotating the secret invalidates the previous value, so update the AI for Work configuration with the new secret.
AI for Work uses the endpoint URL and secret to sign its requests with HMAC.
## Link the connection in AI for Work
1. Open the AI for Work Admin Console, switch to the target workspace, and go to **AI Agents** -> **Autonomous Agents**.
2. Select **Create agent**, then create the agent from scratch.
3. Enter the agent name and the purpose of the app. AI for Work uses the purpose for query routing and training.
1. In **Configure agentic app**, select **Add using credentials**.
2. On the **Link using credentials** dialog, enter the **Endpoint URL** and the **Connection Secret** you copied from Artemis.
3. Select **Run and Save**.
Select **Browse and Link Projects** on the same dialog to pick the app directly from an Agent Platform workspace instead of entering credentials. Use the credentials method when the agent is in a workspace you can't browse from AI for Work, or when you want the connection pinned to a specific Artemis channel connection.
Select **Async** as the mode and test the connection. A successful test reports that the async connection succeeded.
Async mode suits agents that take longer than the 60-second sync timeout. When you use async mode, set the **Callback Base URL** on the Artemis connection's **Configuration** tab so the agent can return responses and proactive notifications to AI for Work.
Select **Continue**, then select **Publish**. Choose whether to publish to everyone in the account or to limited users. For the full set of publishing options, see the [AI for Work custom agents documentation](https://docs.kore.ai/ai-for-work/custom-agents#publishing-your-agent).
## Key values reference
| Value | Where it comes from | Where it's used |
| - | - | - |
| **Endpoint URL** | The **AIforWork Connection Created** dialog in Artemis, or the connection's **Overview** tab | The **Endpoint URL** field on the AI for Work **Link using credentials** dialog |
| **Connection Secret** | The **AIforWork Connection Created** dialog in Artemis; shown only at creation time | The **Connection Secret** field on the AI for Work **Link using credentials** dialog |
| **Callback Base URL** | Your AI for Work instance | The **Callback Base URL** field on the Artemis connection's **Configuration** tab |
## Pass custom metadata
AI for Work can pass custom metadata to the connected Artemis app. In **Configure agentic app**, open **\> Meta Data** and add the metadata you want to send.
Inside Artemis, the expression that reads the metadata differs by surface. The following examples read an `email` key.
| Surface | Expression |
| - | - |
| Agent | `{{_metadata.email}}` |
| Workflow tool | `{{vars._session._metadata.email}}` |
| HTTP tool | `{{session._metadata.email}}` |
## Manage the channel connection
Opening a connection from **Deployments** -> **Channels** -> **AIforWork** shows three tabs: **Overview**, **Configuration**, and **Deployment**. The header shows the connection name, the bound environment, the status, and the channel type, along with **Pause** and delete actions.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | For example: *Active* |
| **Created** | Timestamp the connection was created |
| **Updated** | Timestamp of the last change |
| **Environment** | For example: `dev` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique connection identifier (UUID) |
Under **Setup Instructions**, the tab shows the **Endpoint URL** to enter in AI for Work and the masked **Connection Secret**. Select **Rotate** to issue a new secret. The full secret is shown only at creation time and can't be retrieved.
Under **General**, configure the following.
| Field | Description |
| - | - |
| **Display Name** | The label for the connection. |
| **Provider Verification Strength** | **Weak (default)** or **Strong**. Choose **Strong** only for channels or providers whose identity assertions you trust for stronger continuity and linking decisions. |
Under **AIforWork Settings**, configure the following.
| Field | Description |
| - | - |
| **Callback Base URL** | The base URL where ABL sends async responses and proactive notifications. Required when AI for Work uses the async response mode. |
#### Authorization message
The **Authorization Message** panel controls the message shown when a tool needs the user to authorize. Edit the **Message template** directly, or use the **Insert** buttons to add the most common variables at the cursor.
The default template asks the user to authorize the pending verifications and then lists each one as a numbered profile name with its authorization link.
Expand **Show variables** to see the full set of supported variables.
| Scope | Variables | Notes |
| - | - | - |
| Message scope | `{{total}}`, `{{completed}}`, `{{remaining}}` | Counts for the session's pending authorizations. Use these anywhere in the template. |
| Item scope | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Valid only inside the repeat block. |
| Repeat block | `{{#authorizations}}`, `{{/authorizations}}` | Wraps the item scope variables and repeats the enclosed text once per pending authorization. |
Expand **Show preview** to see the template rendered with sample values. At runtime the platform fills these from the live session's pending authorizations.
Select **Save Changes** to apply any changes.
The **Deployment** tab shows these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Development`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and when it was deployed. |
Select **Save Changes** after switching the environment.
# Set up Genesys
Source: https://koreai.mintlify.app/agent-platform/channels/genesys
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital)
Connect your agent to Genesys Cloud as a Bot Connector so an Architect message flow can hand a conversation to it for intent and slot processing. You install the Bot Connector integration in Genesys, bind its Stream ID and shared secret to Artemis, then call the integration from an Architect flow.
This channel works differently from every other messaging channel. Genesys sends one authenticated POST carrying the user's turn and holds that request open while your agent runs, then reads the reply out of the same HTTP response. There's no callback and no later send API.
The setup requires:
1. [Genesys Cloud setup](#genesys-cloud-setup): install and name the Bot Connector integration, and note its Stream ID.
2. [Artemis channel configuration](#artemis-channel-configuration) using the Stream ID and a shared secret you generate, then copy the generated Bot Connector URL.
3. [Bot Connector configuration in Genesys](#bot-connector-configuration-in-genesys): set that URL and the same secret on the integration, activate it, and call it from an Architect flow.
After you complete the configuration, an Architect flow reaching the **Call Bot Connector** action sends the turn to Artemis, the bound deployment answers inside the same request, and Architect branches on the intent your agent returns.
**Important considerations**
* **The response travels in the same request**: Genesys waits for your agent. Provider timeout and agent execution time have to be compatible, and this channel stays non-streaming by design.
* **Client Secret isn't a Genesys OAuth secret**: for Bot Connector, it's a pre-shared bearer token you invent and configure identically on both sides. Substituting an OAuth integration secret fails unless Genesys sends that same value on the webhook request.
* **Bot Connector needs a subscription change**: Genesys requires you to contact Genesys Cloud Sales before you can install the integration.
* **Architect intents must already exist**: Artemis returns intent names that Genesys matches to branches in your flow. It doesn't create or validate them, so a name that doesn't exist in Architect produces a branch that never fires.
## Prerequisites
* A Genesys Cloud organization with the Bot Connector integration available on your subscription, in the region that owns the Architect flow.
* Admin access in Genesys Cloud, with permission to install integrations and edit Architect flows.
* An Architect message flow where the conversation should reach the agent, or permission to create one.
* Artemis project access with permission to open **Deployments** -> **Channels**, and a deployed agent in the environment you intend to bind.
***
## How Artemis handles Bot Connector requests
Genesys posts to a URL ending in the Stream ID, so Artemis resolves the connection from that path segment and the conversation from the payload.
| Piece | Who owns it | What it does |
| - | - | - |
| **Stream ID** | Genesys | Routes inbound requests. Artemis reads the final path segment and matches it to an active connection. It's a routing identifier rather than a secret. |
| **Client Secret** | You | Authenticates the request. Artemis compares the token Genesys sends with the stored secret using a timing-safe check. |
| Genesys conversation ID | Genesys | Identifies each end-user session inside the connection. Artemis keys the session to it, so reusing it continues the conversation and changing it starts a new one. |
| **Webhook URL** | Artemis | Receives the Bot Connector POST. It ends with the encoded Stream ID, and the generic Genesys route alone can't select a connection. |
Because the exchange is synchronous, the HTTP response carries everything Genesys needs: the reply messages, the bot state, the intent, and whether the task ended. Nothing arrives later.
***
## Genesys Cloud setup
Do this work in Genesys Cloud before you open Artemis. Genesys documents the same screens in [Configure the Genesys Bot Connector integration](https://help.genesys.cloud/articles/configure-the-genesys-bot-connector-integration/).
Sign in to the Genesys Cloud organization and regional domain that own the Architect flow you intend to change.
Genesys organizations are region-specific, so a development organization's integration pointed at production Artemis produces a setup that looks correct but serves the wrong traffic. Confirm the organization before you install anything.
1. Select **Admin**, then under **Integrations**, select **Integrations**. On newer navigation, select **Menu** -> **IT and Integrations** -> **Integrations**.
2. Search for the **Genesys Bot Connector** tile and click **Install**.
3. On the **Details** tab, give the integration a name that identifies the agent and the environment, and add notes if your team uses them.
Pick a distinct name. Architect actions reference integrations by name, so duplicates make it easy to point a flow at the wrong one later.
Don't install the **Genesys Digital Bot Connector** tile by mistake. It's a separate integration with its own Architect action, and Artemis connects through Bot Connector. Keep this integration's lifecycle separate from Open Messaging and Audio Connector integrations too.
Genesys allows up to five Bot Connector integrations per organization, which is the practical ceiling on how many Artemis connections one organization can carry.
Copy the Stream ID Genesys shows for this integration, exactly as it appears.
This becomes the routing identifier in Artemis and the last segment of the webhook URL. Copy it without surrounding whitespace, because a trailing space produces a 404 that looks like a missing connection.
Don't substitute the Genesys organization ID, the integration ID, an OAuth client ID, a conversation ID, or the Architect flow ID. None of them route.
Create a high-entropy random string. You invent this value rather than Genesys issuing it, and you'll enter the identical string in two places: the Artemis connection, and the Bot Connector credentials in Genesys.
Use a different secret per environment, and store it in your secret manager. This token is the only thing standing between the public URL and your agent.
***
## Artemis channel configuration
Create the connection before you configure the integration in Genesys. Artemis resolves the Stream ID and decrypts the secret before it authenticates a request, so an inactive or missing connection returns 404 even when the URL is right.
1. Open Artemis and select the project.
2. Go to **Deployments** -> **Channels** -> **Genesys** -> **+ New**.
3. Configure the following fields:
| Artemis connection field | Source / Notes |
| - | - |
| Display Name | The label operators see in the connections list. Name it for the Genesys organization and the environment it serves, because a project can hold several Genesys connections. Routing ignores this value. |
| Stream ID | The routing key. Artemis writes it into the webhook path and resolves the connection from it. Enter the exact value from the Bot Connector integration, with no leading or trailing whitespace. |
| Client Secret | The shared bearer token you generated. Artemis compares the token on each request with this encrypted value using a timing-safe check. |
| Environment | The published environment whose active deployment answers the Architect flow. Leave the working copy default only while you develop. |
| Provider Verification Strength | How far Artemis trusts Genesys's identity assertion when it links a caller to an existing session. **Weak** suits most deployments. Choose **Strong** only when you trust those assertions enough to base continuity and account-linking decisions on them. |
| Initial Status | Whether the connection handles requests. Set it to **Active**, because an inactive connection returns 404 rather than an authentication error, which sends you looking in the wrong place. |
4. Click **Create**.
5. Open the connection and copy the **Webhook URL** from the **Overview** tab.
The URL ends with the URL-encoded Stream ID. Copy the whole thing, because the generic Genesys route can't identify which connection a request belongs to.
### Validation checklist
| Validation item | Expected result |
| :- | :- |
| Confirm the subscription | The Genesys Bot Connector tile appears in your organization's integration list. |
| Install the integration | Bot Connector integration installed and named distinctly, in the region that owns the flow. |
| Copy the Stream ID | Stream ID copied exactly, with no surrounding whitespace, and distinct from organization, integration, and flow IDs. |
| Create the Artemis connection | Connection saved as Active with the Stream ID and the shared secret. |
| Copy the webhook URL | Bot Connector URL copied from the **Overview** tab, ending with the encoded Stream ID. |
| Configure the integration | URL and matching secret saved on the integration, and its status set to active. |
| Map the Architect outcomes | Intent names in Artemis match branches that already exist in the Architect flow. |
| Wire the Architect action | **Call Bot Connector** action added to the flow, pointing at this integration, with its failure path connected. |
| Run a test interaction | Test turn reaches the agent and Architect branches on the returned intent. See [Verify the connection](#verify-the-connection). |
***
## Bot Connector configuration in Genesys
Return to the integration you installed and finish it with the URL and secret from Artemis, then call it from a flow.
Open the integration's **Configuration** tab and set the Bot Connector URL to the **Webhook URL** from the Artemis connection.
Preserve the encoded Stream ID at the end. Genesys uses this URL to relay each utterance, so a truncated path reaches Artemis and resolves to nothing.
Open the integration's **Credentials** tab and configure the same Client Secret you entered in Artemis as the bearer token.
Artemis prefers the token on an `Authorization: Bearer` header. It also accepts `x-channel-secret`, `x-ingress-secret`, or `x-webhook-secret`. It never accepts a token in the query string, so don't append one to the URL.
Set the integration status to active.
An inactive integration doesn't appear in Architect at all, so this step has to come before you try to add the action.
1. Open the Architect message flow that should reach the agent.
2. From the toolbox, drag a **Call Bot Connector** action into the flow.
3. Set the action's bot integration to the integration you just configured, and set the name and version fields the action asks for.
4. Connect the action's failure path to real handling rather than leaving it unconnected.
Genesys documents the action's inputs, timeouts, and error types in [Call Bot Connector action](https://help.genesys.cloud/articles/call-bot-connector-action/). The failure path matters: Architect takes it on execution errors and connectivity problems, and it also takes a safety path after four consecutive turns return an unchanged intent.
Run Architect's validation and test the flow on a non-production interaction.
Publish only after you've confirmed the URL, the secret, the response timing, and the failure branches. A published flow with an unreachable bot fails in front of customers.
***
## Map Architect outcomes
Artemis returns an intent name to Genesys at the end of each turn, and Architect branches on it. Configure the mapping on the connection's **Configuration** tab, under **Architect outcome mapping**.
Turn on **Enable terminal intent mapping**, then set the intent name Artemis returns for each outcome.
| Outcome | Artemis returns it when |
| - | - |
| Continue conversation | The conversation should stay with this agent for another turn. |
| Escalate to an agent | The conversation should transfer to a human. |
| Close the session | The conversation should end. |
Each outcome takes a **Value source** of either a literal intent name you type, or a project variable that supplies it. Use a project variable when the same agent serves several Genesys flows with different intent vocabularies, and a literal name otherwise.
These names have to match intents and branches that already exist in your Architect flow. Artemis neither creates nor validates them, so a typo produces a branch Architect never takes rather than an error you can see. Agree the names with whoever owns the flow before you enter them here.
***
## Verify the connection
Test each failure mode as well as the happy path, because Architect's branching depends on Artemis returning the right status for each one.
1. Send a text turn with a unique message and a stable Genesys conversation ID.
Confirm an HTTP 200 and one text reply in the response.
2. Send a second turn reusing the same conversation ID, and confirm it resumes the same session.
A different conversation ID should open a separate session.
3. Return an authored quick reply, send its encoded payload back as the button response, and confirm the session resumes with the action value you authored.
4. Send a request with the wrong secret and confirm it returns 401.
5. Send a request to an unknown or inactive Stream ID and confirm it returns 404.
6. Send malformed input and confirm it returns 400.
7. Confirm the Architect flow handles a 503 busy response and an execution failure without looping.
***
## Request and response contract
Genesys carries the whole turn in one exchange, so both halves have required fields.
| Direction | Required content |
| - | - |
| Request | A non-empty Genesys conversation ID and an input message type. A text turn also needs the message text; a structured quick reply needs the button response payload. |
| Response | The reply messages, the bot state, the intent name, and whether the task ended. Genesys reads all of these from the same HTTP response. |
Keep the conversation ID stable across every turn in one Genesys interaction. Replacing it with the Stream ID or an ephemeral request ID resets the conversation context, which looks like the agent forgetting rather than like a configuration error.
Optional session metadata crosses a trust boundary, so Artemis validates it before the agent runs. Send it only in the JSON object shape Artemis expects.
***
## Secure the connection
The Stream ID appears in a public URL and isn't a secret. HTTPS and the shared token protect the endpoint, so never put the token in the URL or in logs.
Use a unique, high-entropy Client Secret for each environment, stored only in the Genesys integration's secure credential configuration and in encrypted Artemis credentials. Rotating it means changing both sides together, because Artemis rejects a request the moment the values diverge.
Prefer the `Authorization: Bearer` header. If your ingress adds or rewrites headers, make sure it preserves the one Genesys sends and strips any untrusted duplicate before the request reaches Artemis. Two competing authorization headers produce a 401 that looks like a wrong secret.
Use separate Genesys integrations, Stream IDs, secrets, Architect flows or versions, and connections for development and production.
***
## Manage the channel connection
Opening a connection shows four tabs. The delete control sits beside the connection name.
**Connection Summary** reports the connection's state and identity.
| Field | Description |
| - | - |
| **Status** | Whether the connection currently handles requests. |
| **Created** and **Updated** | Date and time of creation, and of the last change. |
| **Environment** | The environment whose deployment answers requests. |
| **Source Type** | Reads `Channel Connection`. |
| **Source ID** | Identifies the connection uniquely. Quote it when you raise a support ticket. |
**Setup Instructions** shows the **Webhook URL** to configure in Genesys and the **Stream ID** it ends with, each with a copy control. Compare the Stream ID here with the integration's whenever requests return 404.
Artemis encrypts the secret after you save it, so the field reads as saved rather than showing its value. Leave it empty to keep the current value, or enter a new one to replace it.
| Field | Description |
| - | - |
| **Use Auth Profile** | Draws the credential from a saved auth profile instead of the field below. Prefer it when the same secret serves more than one connection, since a rotation then touches one profile. |
| **Client Secret** | The shared bearer token. Change it here and in the Genesys integration as one coordinated step, because the two have to match on every request. |
Click **Save Credentials** to apply your changes.
Under **General**, rename the connection with **Display Name**, and revisit **Provider Verification Strength** if your trust posture for this organization changes.
Under **Genesys Settings**, **Stream ID** appears read-only. The webhook URL is built from it, so changing it would invalidate the URL configured in Genesys. Create a new connection to serve a different integration.
**Authorization Message** controls what the customer sees when a tool needs them to authorize something before the conversation continues. Write the template with these variables.
| Scope | Variables | Use |
| - | - | - |
| Message | `{{total}}`, `{{completed}}`, `{{remaining}}` | Summarize progress across every pending authorization, typically in a lead-in line above the list. |
| Repeat block | `{{#authorizations}}` and `{{/authorizations}}` | Wrap the section Artemis repeats once per pending authorization. |
| Item | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Identify and link each authorization inside the repeat block. |
**Architect outcome mapping** sets the intent names Artemis returns to Genesys. See [Map Architect outcomes](#map-architect-outcomes).
Click **Save Changes** to apply your changes.
Choose which deployed version of the agent answers requests on this connection.
Under **Agent Version**, set **Environment** to the environment you want the connection to follow. **Active Deployment** then shows the version currently serving traffic and when someone deployed it.
Check this pair when the flow reaches the agent but the agent behaves like an older build. The connection follows whatever the environment currently has deployed, not the version you last edited. Click **Save Changes** after you switch.
***
## Troubleshoot connection issues
| Symptom | Likely cause | Recommended action |
| - | - | - |
| The webhook returns 404 | The Stream ID doesn't match, or the connection isn't active | Confirm the URL ends with the exact Stream ID, check the saved identifier for copied whitespace, and confirm the connection is Active in the intended project. |
| The webhook returns 401 | The token doesn't match, or headers conflict | Send the exact Client Secret through `Authorization: Bearer` or a supported secret header, remove duplicate authorization headers at your ingress, and confirm nobody rotated the secret on one side only. |
| The webhook returns 400 | The request is missing a required field | Include a non-empty conversation ID and an input message type. Text turns also need the message text, and structured actions need the button response payload. |
| The webhook returns 503 busy | Concurrent turns for the same session couldn't acquire the lock | Serialize turns within a conversation and retry according to the Architect failure policy. |
| Genesys times out before a response | Agent execution outlasts the provider timeout | Compare the configured timeout with actual execution duration, investigate model and tool latency, and keep this channel synchronous and non-streaming. |
| Text works but quick replies don't | The response lacks structured entries, or the payload doesn't return | Confirm the response carries quick reply entries and that the selected encoded payload comes back as the button response payload. |
| Conversation context resets between turns | The conversation ID changes each turn | Preserve the same Genesys conversation ID for every turn in one interaction, and don't replace it with the Stream ID or a per-request identifier. |
| Architect never takes the expected branch | The intent name doesn't exist in the flow | Compare the names in **Architect outcome mapping** with the intents and branches in Architect. Artemis doesn't create or validate them. |
| The integration doesn't appear in Architect | The integration isn't active | Set the integration's status to active in Genesys Cloud, then reopen the flow. |
***
## Genesys reference
Use these Genesys pages when you need detail beyond what this article covers.
| Resource | Use it for |
| - | - |
| [About Genesys Bot Connector](https://help.genesys.cloud/articles/about-genesys-bot-connector/) | What the connector supports, including quick replies, cards, and carousels. |
| [Configure the Genesys Bot Connector integration](https://help.genesys.cloud/articles/configure-the-genesys-bot-connector-integration/) | Installing the integration, its tabs, and activating it. |
| [Call Bot Connector action](https://help.genesys.cloud/articles/call-bot-connector-action/) | Action inputs, response timeouts, session variables, error types, and failure paths. |
| [About Genesys Digital Bot Connector](https://help.genesys.cloud/articles/about-genesys-digital-bot-connector/) | Telling the two connectors apart, if you're unsure which one your flow uses. |
**Related articles:**
* [Set up Zendesk](/agent-platform/channels/zendesk)
* [Set up WhatsApp](/agent-platform/channels/whatsapp)
# Genesys Audio Connector
Source: https://koreai.mintlify.app/agent-platform/channels/genesys-audio-connector
[Back to voice channels](/agent-platform/channels#voice-channels)
Create a project-owned Genesys Audio Connector connection, choose a Voice mode, configure that mode's speech settings, wire the connection into Genesys Architect, and validate live audio before enabling traffic.
**Voice mode can't be changed after the connection is created.** Choose **Pipeline** for independent speech-to-text, LLM, and text-to-speech providers, or **Realtime** for a single speech-to-speech (S2S) model with lower latency. If you need the other architecture later, create a new connection.
### Where setup happens
Setup moves back and forth between Genesys Cloud and Studio. Here's where each step happens:
| Step | Where |
| - | - |
| [Prepare Genesys Cloud and authentication](#prepare-genesys-cloud-and-authentication) | Genesys Cloud |
| [Create the connection in Studio](#create-the-connection-in-studio) | Studio |
| [Configure the connection](#configure-the-connection) | Studio |
| [Wire the Genesys Architect flow](#wire-the-genesys-architect-flow) | Genesys Cloud |
| [Run live validation before enabling traffic](#run-live-validation-before-enabling-traffic) | Studio + Genesys Cloud |
## Prepare Genesys Cloud and authentication
Sign in to the regional Genesys Cloud organization that owns the voice deployment. Studio uses this region to select the matching Genesys API host.
In Genesys Cloud Admin, identify the voice deployment associated with the intended Architect flow and copy its Voice Deployment ID.
Generate a project-specific Audio Connector API key for this connection. Configure the matching value in Genesys when you wire the Architect flow.
Use separate API keys, voice deployments, and connections for development and production. Never share one connection's credentials across environments.
## Create the connection in Studio
To configure a Genesys Audio Connector channel, go to **Deployments** -> **Channels** -> **VOICE** -> **Genesys Audio Connector** -> **+ New** and complete the following steps in order.
Give the connection a label, for example `Genesys Audio Connector Production`.
Enter the exact Genesys Organization ID and select the region that owns it, for example `US East (N. Virginia)`. A mismatched region can make valid credentials appear unavailable.
Paste the voice deployment ID associated with the intended Architect flow.
Select **Pipeline** or **Realtime**. This choice is permanent for the connection - see the note above.
Start in **Diagnostics only** so the Testing tab can collect evidence without treating the setup as production-ready. Switch to **Enabled** only after validation passes.
Enter the project-specific Audio Connector API key. **Webhook Signature Secret** is optional at creation - set it when Genesys is configured to sign outbound requests.
Set the maximum duration for a single voice session, in seconds (default: `1800`).
| Field | Description |
| - | - |
| **Environment** | Select an environment to follow its active deployment, or leave the default to use the working copy. |
| **Provider Verification Strength** | **Weak** is the default. Choose **Strong** only for channels or providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. |
Select **Create**. The next step - configuring speech settings for your chosen Voice mode - happens after creation, on the connection's **Configuration** tab.
### Connection fields reference
| Field | Description |
| - | - |
| **Display Name** | A label for the connection. |
| **Genesys Organization ID** | Your Genesys Cloud organization identifier. |
| **Allowed Region** | The Genesys region this connection is scoped to. |
| **Voice Deployment ID** | The voice deployment associated with the intended Architect flow. |
| **Voice mode** | **Pipeline** (independent STT/LLM/TTS providers) or **Realtime** (single S2S model). Permanent after creation. |
| **Activation Mode** | **Diagnostics only** (default, safe - setup and validation, no production traffic), **Enabled** (processes live calls), or **Disabled**. |
| **API Key** | A project-specific Audio Connector API key. |
| **Webhook Signature Secret** | Optional at creation. Configure the identical value in Genesys when signed HTTP signaling is enabled. Referred to as **HTTP Message Signature Secret** later, on the Credentials tab - same value, different label. |
| **Max Session Seconds** | Maximum duration for a single voice session, in seconds. Default: `1800`. |
## Configure the connection
After creation, open the connection and go to its **Configuration** tab to set up speech behavior for the Voice mode you chose.
| Field | Description |
| - | - |
| **Provider** | The speech-to-text credential set to use, for example `Deepgram Speech Credentials`. |
| **Language** | The recognition language, for example `Multilingual`. |
| **STT Model** | The recognition model, for example `Nova 3 (latest, most accurate)`. |
| **Minimum STT confidence** | Reject transcripts below this confidence (0–1) and treat them as no-input. `0` disables the gate. Provider-relative - calibrate per channel. |
| **Custom vocabulary** | Domain-specific terms to bias recognition toward. Select **Add phrase** and set a **Phrase** (for example `ACME, policy number`) and optional **Boost**. Deepgram Nova-3 and Flux use keyterm prompting and ignore boost; older Deepgram models use keyword intensifiers. |
Optionally enable **A different voice per language** to give each language its own voice instead of one voice for every caller. Then set:
| Field | Description |
| - | - |
| **Provider** | The text-to-speech credential set to use, for example `ElevenLabs (TTS) Credentials`. |
| **Language** | The synthesis language, for example `Arabic`. |
| **Voice** | The voice used for synthesized speech. |
| **TTS Model** | The synthesis model, for example `Eleven Multilingual v2`. |
If you selected an ElevenLabs voice, an **ElevenLabs Voice Settings** panel lets you override **Speed**, **Stability**, **Similarity boost**, **Style exaggeration**, and **Speaker boost** for this connection, plus a **Preview Voice** field to hear the configured voice before saving.
| Field | Description |
| - | - |
| **Barge-in** | Allow the caller to interrupt the agent while it is speaking. |
| **Speech Timeout (ms)** | The time the agent waits for caller speech before it times out, for example `3000`. |
| **Welcome Message** | The opening prompt the agent speaks, such as `Hello! How can I help you today?`. |
| **Noise isolation** | Apply server-side noise reduction to inbound call audio to filter out background voices and ambient noise. |
| Field | Description |
| - | - |
| **Transfer Queue Header Key** | SIP transfer header name that carries the queue, for example `X-Queue`. Must match what Genesys expects. Required for escalate-to-human. |
| **Transfer Queue Name** | The Genesys queue used when the agent transfers a caller to a human, for example `Kore Inbound`. Must match a queue configured in Genesys. |
Select **Save Changes**.
Under **Realtime LLM Voice Settings**, select an **S2S Provider**. Only the panel for the selected provider applies to this connection.
**Azure OpenAI Realtime, Grok Realtime, Google Gemini Live, and OpenAI Realtime** each require a **Realtime model connection** - a project-level model pinned to this channel. Amazon Nova Sonic does not use this field; it authenticates via Voice Service credentials instead.
To make a model available here:
Configure the model under [Voice Services](/agent-platform/administration/ai-configuration#voice-services), then add it to the [model catalog](/agent-platform/administration/ai-configuration#model-catalog) as an LLM provider.
Go to **Settings** -> **Model** and add the model you configured at the workspace level.
Once added, the model becomes selectable in the **Realtime model connection** dropdown below.
| Field | Description |
| - | - |
| **Realtime model connection** | The project-pinned realtime model this provider uses. Shows `No project model selected` until a workspace admin adds one following the steps above; once selected, shows the model name and provider (for example `GPT Realtime 2 · openai`). |
| Field | Description |
| - | - |
| **Voice** | The synthesized voice, for example `Tiffany (default)`. |
| **Model** | The Amazon Bedrock model ARN, for example `arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-2-sonic-v1:0`. |
| **Maximum tokens** | The maximum response length, in tokens. Default: `1024`. |
| **Temperature** | Response creativity. Default: `0.70`. |
| **Top P** | Nucleus sampling threshold. Default: `0.90`. |
| **Endpointing sensitivity** | How quickly the model treats the caller as finished speaking - `Low`, `Medium`, or `High`. `Medium` is the general-conversation default; `High` responds sooner but may interrupt callers; `Low` allows more pause before responding. |
| Field | Description |
| - | - |
| **Voice** | The synthesized voice, for example `Marin (recommended)`. |
| **Temperature** | Response creativity, from `0.6` (Focused) to `1.2` (Creative). Default: `0.8`. |
Under **Turn Detection** - tunes how Azure OpenAI detects speech starts and decides when the caller has finished speaking:
| Field | Description |
| - | - |
| **Speech detection sensitivity** | Lower values pick up quieter speech; higher values reduce background-noise triggers. Default: `0.5`. |
| **Silence Duration (ms)** | Quiet time before Azure OpenAI treats the caller turn as complete. Default: `700`. |
| **Prefix Padding (ms)** | Audio kept before detected speech so the beginning of the caller utterance is preserved. Default: `300`. |
| Field | Description |
| - | - |
| **Model** | The Grok Realtime model, for example `grok-voice-latest (recommended)`. |
| **Voice** | The synthesized voice, for example `Ara (default)`. |
| **Temperature** | Response creativity, from `0.0` (Focused) to `2.0` (Creative). Default: `1`. |
| **Threshold** | Turn-detection sensitivity, from `0.0` (Sensitive) to `1.0` (Conservative). Default: `0.5`. |
| **Silence Duration (ms)** | Quiet time before the caller turn is treated as complete. Default: `500`. |
| **Prefix Padding (ms)** | Audio kept before detected speech so the start of the caller's utterance is preserved. Default: `300`. |
The xAI Grok Realtime API provides low-latency voice conversations with function calling support.
| Field | Description |
| - | - |
| **Model** | The Gemini Live model, for example `Gemini 3.1 Flash Live (Latest)`. |
| **Voice** | The synthesized voice, for example `Puck - Upbeat`. |
| **Pending tool fillers** | Allow Gemini Live to acknowledge long-running project tools while the original tool call remains pending. |
| **Temperature** | Response creativity, from `0.0` (Deterministic) to `2.0` (Creative). Default: `1`. |
Under **Voice Activity Detection** - controls when Gemini starts listening to a caller turn and how long it waits before closing that turn:
| Field | Description |
| - | - |
| **Start Sensitivity** | Higher detects speech sooner but may trigger on noise; lower waits for a stronger speech signal. Default: `Default`. |
| **End Sensitivity** | Higher ends turns sooner for lower latency; lower allows longer pauses before Gemini responds. Default: `Default`. |
| **Silence Duration (ms)** | Milliseconds of non-speech before Gemini closes the caller turn. Increase to allow pauses; decrease for faster responses. Default: `100`. |
| **Prefix Padding (ms)** | Milliseconds of detected speech required before Gemini commits a speech start. Lower values catch shorter utterances but can increase false starts. Default: `20`. |
Google Gemini Live provides multimodal voice conversations with low latency.
| Field | Description |
| - | - |
| **Model** | The OpenAI Realtime model, for example `gpt-realtime-1.5 (recommended)`. |
| **Voice** | The synthesized voice, for example `Marin (recommended)`. |
Under **Turn Detection** - tunes how OpenAI detects speech starts and decides when the caller has finished speaking:
| Field | Description |
| - | - |
| **Speech detection sensitivity** | Lower values pick up quieter speech; higher values reduce background-noise triggers. Default: `0.5`. |
| **Silence Duration (ms)** | Quiet time before OpenAI treats the caller turn as complete. Default: `700`. |
| **Prefix Padding (ms)** | Audio kept before detected speech so the beginning of the caller utterance is preserved. Default: `300`. |
The OpenAI Realtime API provides low-latency voice conversations with function calling support.
The following fields apply regardless of which S2S provider you select:
| Field | Description |
| - | - |
| **Noise isolation** | Apply server-side noise reduction to inbound call audio to filter out background voices and ambient noise. |
| **Transfer Queue Header Key** | SIP transfer header name that carries the queue, for example `X-Queue`. Must match what Genesys expects. Required for escalate-to-human. |
| **Transfer Queue Name** | The Genesys queue used when the agent transfers a caller to a human, for example `Kore Inbound`. Must match a queue configured in Genesys. |
Select **Save Changes**.
## Wire the Genesys Architect flow
Open the connection's **Testing** tab and copy the **AudioHook Callback URL** - a `wss://` URL with an embedded connection ID, for example `wss://korevg-np.audiosocket.kore.ai/audiosocket/genesysconnect/`. Preserve the full URL and connection ID; do not substitute a generic Runtime WebSocket.
In the intended Architect flow, add or update the Audio Connector action and paste the callback and API key into the corresponding integration fields.
Keep the configured PCMU framing, sample rate, channel count, and media direction aligned with the platform contract. Transcoding or buffering at an intermediate proxy adds latency and can corrupt audio.
Capture normal completion, caller hangup, gateway error, and fallback outcomes so Architect can continue or exit the flow deterministically.
## Run live validation before enabling traffic
Use a dedicated Architect test flow and test number. Confirm session creation, greeting playback, caller speech, agent response, barge-in, and clean hangup.
Confirm PCMU framing, bidirectional audio, queue behavior, packet loss, and disconnect variables under normal speech, silence, interruption, and rapid turn-taking.
Record audio jitter p95, first audio p95, STT first partial p95, TTS first audio p95, and representative end-to-end turn latency. Investigate regressions before promotion.
On the **Testing** tab, record AudioHook and KoreVG session IDs, Architect flow name and version, Runtime trace IDs, sandbox evidence, dashboard and alert references, concurrency, and fallback results, then select **Save Evidence**.
Mark validation **Passed**, retain the evidence, then change **Activation Mode** to **Enabled**. Keep a tested rollback or fallback path available.
### Promotion gate checklist
Before switching a connection from **Diagnostics only** to **Enabled**, confirm all four areas below have passing evidence:
| Gate | What to confirm |
| - | - |
| **1. Architect sandbox call** | Greeting, caller speech, agent audio, barge-in, and hangup all work. AudioHook and KoreVG session IDs are captured. |
| **2. Media contract** | PCMU framing, bidirectional audio, and backpressure hold up under packet loss and disconnect scenarios. |
| **3. Latency and capacity evidence** | Audio jitter p95, first audio p95, STT first partial p95, TTS first audio p95, and concurrency are all recorded. |
| **4. Promote Diagnostics only -> Enabled** | Passed evidence, trace IDs, flow version, dashboards, alerts, and a tested rollback or fallback path are all in place. |
## Manage the channel connection
Opening a connection from **Deployments** -> **Channels** -> **VOICE** -> **Genesys Audio Connector** shows six tabs: **Overview**, **Credentials**, **Configuration**, **Deployment**, **Testing**, and **Activity**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | For example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | For example: `Working Copy (draft)` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique channel identifier (UUID) |
Under **Setup Instructions**, the tab shows the **Genesys Organization ID** and the **SIP URI** for reference. Select the copy icon next to either value to copy it.
Credentials are saved and encrypted after creation; fields show placeholder text and accept new values only if you want to update them.
| Field | Description |
| - | - |
| **Use Auth Profile** | Toggle on to authenticate through a saved Auth Profile instead of an API key. |
| **API Key** | Shown as *(saved - enter to update)*; leave blank to keep the current value. |
| **HTTP Message Signature Secret** | Shown as *(saved - enter to update)*; leave blank to keep the current value. This is the same secret set as **Webhook Signature Secret** at creation. |
Select **Save Credentials** to apply any changes.
Shows **General** (Display Name, Provider Verification Strength) plus the Pipeline or Realtime speech settings described in [Configure the connection](#configure-the-connection) above, editable at any time. Select **Save Changes** to apply.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and date, or `Working Copy (draft)` if nothing has been deployed to an environment yet. |
If the connection is running on the working copy, a banner reads: *"This channel is running on the Working Copy (draft). Working copy is intended for testing. For a stable production setup, deploy your agent to an environment and bind this channel to that environment."*
Select **Save Changes** after switching the environment or version.
The **Audio Connector Health** panel shows:
| Field | Description |
| - | - |
| **AudioHook Target** | The execution target for this connection, for example `KoreVG (SAVG audiosocket)`. |
| **Last Success** / **Last Failure** | Timestamps of the most recent diagnostic outcomes, or a prompt such as *Live Architect validation required*. |
| **Provider URL** | The underlying provider endpoint the connection routes to. |
| **AudioHook Callback URL** | The full `wss://` URL to configure in the Architect Audio Connector action. |
The **Live Validation Evidence** panel records the results of the checks in [Run live validation before enabling traffic](#run-live-validation-before-enabling-traffic).
| Field | Description |
| - | - |
| **Validated At** | Timestamp of the last passing validation. |
| **Trace IDs** | Number or list of Runtime traces captured during validation. |
| **Certification Status** | For example: `Not started`. |
| **Runbook Reference** / **Dashboard Reference** / **Alert Reference** | Links or IDs for the operator runbook, dashboard, and alert configuration used during validation. |
| **Runtime Trace IDs** / **Sandbox Evidence References** | Free-text fields for trace IDs and sandbox evidence links. |
| **Validation Summary** | Free-text summary of the validation outcome. |
| **Audio Runtime Target** | The runtime target validated, for example `KoreVG (SAVG audiosocket)`. |
| **Architect Flow Name** / **Architect Flow Version** | The Architect flow and version used during validation. |
| **AudioHook Session ID** / **KoreVG Session ID** | Session identifiers captured during the validation call. |
| **PCMU framing verified** / **Backpressure verified** / **Disconnect variables verified** / **Fallback path verified** | Checkboxes confirming each part of the media contract. |
| **Audio Jitter P95 (ms)** / **First Audio P95 (ms)** / **STT First Partial P95 (ms)** / **TTS First Audio P95 (ms)** | Recorded latency metrics from validation. |
| **Max Concurrent Sessions** / **Packet Loss (%)** | Recorded capacity and reliability metrics from validation. |
Select **Save Evidence** to record your results.
## Security, capacity, and reliability boundaries
Keep the API key and HTTP Message Signature Secret in Genesys and encrypted Studio credentials. Use distinct values per environment and rotate exposed credentials.
**Audio** - Don't log raw audio or sensitive transcript content. Use scrubbed trace identifiers and aggregate voice phase metrics for operational debugging.
**Capacity** - Validate the expected concurrent-session ceiling and queue pressure. KoreVG has bounded per-session queues and environment-level capacity controls.
**Fallback** - Define what Architect does when the AudioHook can't connect, media stalls, STT or TTS fails, or the agent exceeds its response budget.
**Observability** - Monitor connection failures, audio jitter, packet loss, STT, LLM, and TTS phase latency, session termination reasons, and fallback activation.
# Genesys Open Messaging
Source: https://koreai.mintlify.app/agent-platform/channels/genesys-open-messaging
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital-channels)
Deploy your agent through Genesys Cloud Open Messaging. Connect your Genesys Open Messaging integration to the platform by configuring the required provider details, authentication, and signed webhook.
**Webhook Connection Token and Webhook Signature Secret are different.** The connection token identifies the connection through the webhook URL, while the signature secret verifies the authenticity of incoming requests.
## Create or select an Open Messaging integration in Genesys Cloud
Sign in to the Genesys Cloud organization that owns the Open Messaging integration. Select the correct Genesys region because the platform uses it to connect to the corresponding Genesys Platform API host.
In Genesys Cloud, create or select the required Open Messaging integration and copy its **Integration ID**.
Do not use a Bot Connector Stream ID, Audio Connector integration ID, organization ID, or OAuth client ID.
Create an active Auth Profile for the Genesys connector using OAuth 2.0 Client Credentials or Bearer authentication. The platform uses this profile for outbound Genesys Platform API calls.
Ensure that the Genesys OAuth client has the permissions required to send Open Messaging inbound messages, structured responses, and receipts.
Use separate clients, integrations, and Auth Profiles for development and production environments.
## Integrate the Genesys Open Messaging connection
Open Artemis, select the required project, and go to **Deployments** -> **Channels** -> **MESSAGING** -> **Genesys Open Messaging** -> **+ New**.
Enter the required provider and authentication details.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection, for example `Genesys Open Messaging Production`. Up to 255 characters. |
| **Genesys Region** | The region that hosts your Genesys Cloud organization. |
| **Organization ID** | The identifier for your Genesys Cloud organization. |
| **Integration ID** | The Open Messaging Integration ID. |
| **Activation Mode** | Select **Diagnostics only** initially to validate the connection before processing live traffic. |
| **Delivery Mode** | Keep **External bridge** unless a different delivery mode is explicitly required. External bridge is the standard bot-facing channel — it's what makes the connection available to the Genesys Open Messaging agent-transfer handoff. |
| **Genesys Auth Profile** | An active Auth Profile configured for the Genesys integration. |
The Integration ID is used as the external identifier for outbound provider calls. The selected Auth Profile must be active and configured for the Genesys integration.
| Field | Description |
| - | - |
| **Environment** | Select an environment to follow its active deployment, or leave the default to use the working copy. |
| **Provider Verification Strength** | **Weak** is the default. Choose **Strong** only for channels or providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. |
When you create the connection, the platform generates the following values.
| Value | Description |
| - | - |
| **Webhook Connection Token** | Identifies the connection and is included in the connection-specific webhook URL. |
| **Webhook Signature Secret** | Verifies incoming webhook requests using HMAC-SHA256. |
Copy the generated webhook endpoint and signature secret before closing the confirmation screen. These values are required when configuring the Genesys Open Messaging integration.
## Configure the webhook in Genesys Cloud
In your Genesys Open Messaging integration, enter the complete public HTTPS webhook URL generated for the connection.
The URL must include the connection token. The generic webhook path cannot identify the required connection.
Enter the Webhook Signature Secret generated by the Artemis as the signature secret for the Genesys integration.
Do not use the Genesys OAuth client secret or the Webhook Connection Token as the signature secret.
Incoming requests must be signed using **HMAC-SHA256** over the exact raw request body. Runtime validates the signature before processing the request.
Do not parse and reserialize the request body before signature validation because changing the request bytes can cause a valid signature to fail.
## Test the connection
Once the channel is created, select it, then go to the **Testing** tab.
Open the connection's testing or diagnostics area and run the signature validation. This confirms that the configured signature secret and supported signature format are correct.
Run the inbound message diagnostic and confirm that Runtime resolves the connection and processes the incoming message correctly.
Run the receipt diagnostic to confirm that the Auth Profile can authenticate and communicate with the configured Genesys receipt endpoint.
After all diagnostics pass, change **Activation Mode** to **Enabled**. Test a live message, a quick reply, and a follow-up message in the same Genesys conversation.
## Manage the channel connection
Opening a connection from **Deployments** -> **Channels** -> **MESSAGING** -> **Genesys Open Messaging** shows six tabs: **Overview**, **Credentials**, **Configuration**, **Deployment**, **Testing**, and **Activity**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | For example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | For example: `staging` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique channel identifier (UUID) |
| **Integration ID** | The bound Open Messaging Integration ID, shown for reference. |
Under **Setup Instructions**, the tab shows the connection-specific webhook endpoint to paste into the Genesys Open Messaging integration.
Credentials are saved and encrypted after creation; fields show placeholder text and accept new values only if you want to update them.
| Field | Description |
| - | - |
| **Use Auth Profile** | Toggle on to authenticate through a saved Auth Profile. |
| **Genesys Auth Profile** | Picker to select or change the bound Auth Profile. |
| **Webhook Signature Secret** | Shown as *(saved — enter to update)*; leave blank to keep the current value. |
The **Webhook Connection Token** isn't editable here — it's embedded in the webhook URL and can't be changed after creation. To rotate it, create a new connection.
Select **Save** to apply any changes.
| Field | Description |
| - | - |
| **Display Name** | The connection's label. |
| **Provider Verification Strength** | **Weak** or **Strong** — see [Complete common connection settings](#integrate-the-genesys-open-messaging-connection). |
| **Integration ID** | The bound Open Messaging Integration ID. |
Select **Save Changes** to apply any changes.
| Field | Description |
| - | - |
| **Environment** | Selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and date, or a **Working Copy** warning if nothing has been deployed to this environment yet. |
Select **Save Changes** after switching the environment or version.
The **Diagnostics** panel runs the checks described in [Test the connection](#test-the-connection).
| Field | Description |
| - | - |
| **Last Success** / **Last Failure** | Timestamps of the most recent diagnostic outcomes. |
| **Endpoint** | The webhook endpoint under test. |
| **Signature Secret** | The secret used to sign the test request. |
| **Customer ID** | The test conversation's customer identifier. |
| **Message** | The test message body. |
| **Originating Message ID** | The test message's originating ID. |
| **Run Signature** | Runs the webhook signature validation. |
| **Run Inbound** | Runs the inbound message diagnostic. |
| **Run Receipt** | Runs the delivery or read receipt diagnostic. |
The **Validation Evidence** panel records the results and supporting evidence from those runs.
| Field | Description |
| - | - |
| **Validated At** | Timestamp of the last passing validation. |
| **Trace Count** | Number of Runtime traces captured during validation. |
| **Status** | Overall validation status. |
| **Operator Runbook Reference** | Link or ID for the operator runbook used during validation. |
| **Operator Dashboard Reference** | Link or ID for the operator's monitoring dashboard. |
| **Operator Alert Reference** | Link or ID for the operator's alert configuration. |
| **Runtime Trace IDs** | Trace IDs captured during validation runs. |
| **Sandbox Evidence References** | Links or IDs for sandbox test evidence. |
| **Summary** | Free-text summary of the validation outcome. |
| **Max Concurrent Sessions** | Peak concurrent sessions observed during validation. |
| **Packet Loss Percent** | Observed packet loss during validation, if applicable. |
| **Delivery Mode** | The delivery mode validated (for example, External bridge). |
| **Provider Conversation ID** | The Genesys conversation ID used during validation. |
| **Signature Diagnostic ID** / **Inbound Diagnostic ID** / **Receipt Diagnostic ID** | IDs referencing each diagnostic run. |
| **Automated Provisioning Verified** | Whether automated provisioning was confirmed during validation. |
| **Chat Round Trip P95 ms** | The 95th-percentile round-trip latency observed during validation. |
# Set up Instagram
Source: https://koreai.mintlify.app/agent-platform/channels/instagram
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital)
Connect an Instagram professional account to your agent so people reach it through Instagram direct messages. Instagram messaging runs on the Messenger Platform. You prepare a Meta app and a Facebook Page alongside the Instagram account, then register a signed webhook that Meta verifies before it sends anything.
The setup requires:
1. [Instagram and Meta setup](#instagram-and-meta-setup): Confirm the account is professional, link it to a Facebook Page, add the Instagram product to a Meta app, and collect the account ID, Page Access Token, and App Secret.
2. [Artemis channel configuration](#artemis-channel-configuration) using those values, then copy the generated webhook URL.
3. [Webhook configuration in Meta](#webhook-configuration-in-meta): Verify the callback and subscribe the professional account to the message events.
After the configuration is complete, a direct message to the professional account opens a session in Artemis, the bound deployment answers it, and the reply returns to the same Instagram conversation.
**Important considerations**
* **Instagram messaging runs through a Facebook Page**: The Page issues the access token that authorizes outbound replies, so the Instagram account, the Page, the Meta app, the App Secret, and the webhook subscription all have to belong to one linked set. Values from two apps produce failures that look unrelated to each other.
* **Three Meta identifiers compete for one field**: Artemis routes on the Instagram Account ID. The Facebook Page ID, the Meta App ID, and the Instagram-scoped ID that identifies a customer all appear nearby and none of them route.
* **Create the connection before the webhook**: Meta's verification challenge carries the Verify Token but no account ID, so Artemis finds the connection by the stored token. An unsaved connection fails verification.
* **Development mode limits who can test**: Only assigned app roles and eligible testers can exercise the integration until you complete permission review and switch the app to Live.
## Prerequisites
* An Instagram **Business** or **Creator** account. A personal account can't support this integration, and converting it is the first step below.
* A Facebook Page linked to that Instagram account, on which your account can perform the messaging and moderate tasks. Meta requires both tasks of whoever generates the Page Access Token.
* A Meta developer account with permission to create a business app, add the Instagram product, and reveal the App Secret.
* Artemis project access with permission to open **Deployments** -> **Channels**, and a deployed agent in the environment you intend to bind.
***
## How Artemis routes Instagram events
Meta sends every event for the account to one generic URL, so Artemis identifies the connection from the payload rather than the path. It reads the Instagram account ID out of the signed event and matches it to a connection.
| Piece | Who owns it | What it does |
| - | - | - |
| **Instagram Account ID** | Meta | Routes inbound events. Artemis reads the account ID from each signed webhook and matches it to this connection. |
| **Page Access Token** | Meta | Authorizes outbound replies. Artemis sends to the account's messages endpoint using it as a bearer token, so the token has to belong to the Page linked to this account and carry the Instagram messaging permission. |
| **App Secret** | Meta | Verifies the `X-Hub-Signature-256` header on every inbound POST. Artemis computes the HMAC over the unchanged request body and rejects anything that doesn't match. |
| **Verify Token** | You | Answers Meta's one-time verification challenge when you save the callback URL. It does nothing after that. |
| **Webhook URL** | Artemis | Receives events. The route carries no account ID, because the account ID travels in the payload. |
Artemis scopes each conversation to the account and the sender together, so one person messaging two of your professional accounts holds two separate sessions.
***
## Instagram and Meta setup
Do this work in Instagram and Meta before you open Artemis. Meta covers the same ground in its [Instagram messaging get started guide](https://developers.facebook.com/docs/messenger-platform/instagram/get-started).
1. Open the Instagram app and go to your profile.
2. Open the menu, then **Settings and privacy**.
3. Find the account type setting and switch the account to **Business** or **Creator**.
Instagram messaging works only on a professional account, and the [Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview/) sets out what each account type supports. A personal account can't support this integration at all, so confirm this before you spend time in the App Dashboard.
1. In Instagram, open the professional account settings and find the linked accounts or connected Page setting.
2. Connect the Facebook Page that will issue the Page Access Token.
3. Confirm the link from the Page side as well, so both surfaces agree.
The Page is what makes messaging work. Meta routes Instagram conversations through the linked Page, which is why the token you use later belongs to the Page rather than to Instagram.
1. Sign in to [Meta for Developers](https://developers.facebook.com/) and open [My Apps](https://developers.facebook.com/apps/).
2. Create an app, choosing the business messaging use case, or open the app that will own this connection.
3. Add the Instagram product and configure Instagram messaging for the linked Page and account.
Keep development and production on separate apps and accounts, because app mode and webhook subscriptions belong to the app.
1. Grant the app, its testers, and any system users the Instagram messaging and Page permissions they need.
2. Complete any permission review Meta requires for your current app mode.
3. Generate a Page Access Token for the linked Page and copy it.
Meta requires the `instagram_manage_messages` permission on the token, and requires that whoever requests it can perform the messaging and moderate tasks on the Page. A token generated without both produces a clean-looking connection that fails on the first reply.
Token lifetime follows the user token behind it. A short-lived user token yields a Page token good for about an hour. A long-lived user token yields one with no expiry, so use the long-lived path for anything beyond a first test.
Artemis routes on the numeric professional account ID, which is neither the `@username` nor the Page ID.
Meta surfaces it in the Instagram messaging setup area of the App Dashboard alongside the connected account. You can also query the linked Page for its connected Instagram professional account in the Graph API Explorer, which returns the same numeric ID.
Don't substitute the `@username`, the Facebook Page ID, the Meta App ID, a profile URL, or the Instagram-scoped ID. That last one, the IGSID, appears in every inbound event and identifies the customer: Meta issues each person a different scoped ID for each account they message.
1. In the left menu of the App Dashboard, go to **App settings** -> **Basic**.
2. Click **Show** beside **App secret**.
3. Re-enter your Facebook password when Meta asks, then copy the value.
Take it from the same Meta app that owns the Instagram webhook. An App Secret from a neighboring app produces a signature failure on every event, which is a confusing failure to trace because nothing else looks wrong.
Invent a long random string. Meta doesn't issue this one, so you create it and enter the same value twice: once in Artemis, once in the Meta webhook form.
Use a different value per environment, and treat it as a secret rather than a label. It's your shared challenge secret, distinct from both the Page Access Token and the App Secret.
While the app sits in development mode, only accounts holding an app role or an eligible Instagram tester role can exercise the integration.
Before ordinary people can message the account, complete permission review and switch the app to Live using the control at the top of the App Dashboard. A successful test with a role-holding account proves nothing about public access.
***
## Artemis channel configuration
Create the connection before you configure the callback in Meta. Meta's verification request carries the Verify Token but no account ID, so Artemis resolves the challenge from the stored token and needs the connection to exist and be active.
1. Open Artemis and select the project.
2. Go to **Deployments** -> **Channels** -> **Instagram** -> **+ New**.
3. Configure the following fields:
| Artemis connection field | Source / Notes |
| - | - |
| Display Name | The label operators see in the connections list. Name it for the account and the environment it serves, because a project can hold several Instagram connections. Routing ignores this value. |
| Instagram Account ID | The routing key. Enter the numeric professional account ID. From the Instagram messaging area of the Meta App Dashboard, or from the linked Page in the Graph API Explorer. |
| Page Access Token | Authorizes outbound replies. Use the token generated for the Facebook Page linked to this Instagram account, carrying the Instagram messaging permission. |
| App Secret | Verifies the signature Meta puts on each inbound event. From **App settings** -> **Basic** in the same Meta app that owns the webhook. |
| Verify Token | The string you invented. Enter the identical value in Meta when you register the callback. |
| Environment | The published environment whose active deployment answers incoming messages. Leave the working copy default only while you develop. |
| Provider Verification Strength | How far Artemis trusts Meta's identity assertion when it links a person to an existing session. **Weak** suits most deployments. Choose **Strong** only when you trust those assertions enough to base continuity and account-linking decisions on them. |
| Initial Status | Whether the connection starts handling messages. Set it to **Active**, because Meta's verification challenge resolves only when the connection is already active. |
4. Click **Create**.
5. Open the connection and copy the **Webhook URL** from the **Overview** tab.
Copy it exactly. The route carries no account identifier, and adding one breaks the callback rather than refining it.
### Validation checklist
| Validation item | Expected result |
| :- | :- |
| Convert the Instagram account | Account shows as Business or Creator and is eligible for Instagram messaging. |
| Link the Facebook Page | Instagram account and Page show as linked from both sides, with messaging and moderate tasks available to you. |
| Prepare the Meta app | Instagram product added, permissions assigned, and a non-expired Page Access Token generated for the linked Page. |
| Collect the routing identifier | Numeric Instagram Account ID copied, distinct from the `@username`, Page ID, and App ID. |
| Create the Artemis connection | Connection saved as Active with the account ID, Page Access Token, App Secret, and Verify Token. |
| Register the callback | Meta reports the webhook verified, with `messages` and `messaging_postbacks` subscribed and the intended account connected. |
| Bind a deployment | The connection's **Deployment** tab shows a runnable active deployment. |
| Send a real message | Test direct message reaches the agent and the reply returns once. See [Verify the connection](#verify-the-connection). |
***
## Webhook configuration in Meta
Registration has two halves, and completing only the first is the most common way a setup fails silently. Meta verifies the URL whether or not you subscribe to anything.
1. In the same Meta app, open the Instagram webhook configuration.
2. Paste the Artemis **Webhook URL** exactly as shown.
3. Enter the same **Verify Token** you saved in Artemis.
4. Click the control that verifies and saves the callback.
Meta sends a subscribe challenge and expects your Verify Token echoed back. Artemis returns the challenge only when the token matches an active Instagram connection. A failure here points at the token, the connection status, or public reachability over HTTPS rather than at the URL itself.
Don't append an account ID, a connection ID, a query string, or a trailing segment.
Subscribe the Instagram webhook to `messages` and `messaging_postbacks`, then save the subscription.
The `messages` field carries text, attachments, and quick-reply payloads, so it's required even for an agent that only handles text. `messaging_postbacks` carries the action callbacks behind buttons and template elements.
Confirm the app connects to the same Instagram account, and that the Page subscription includes it.
Artemis resolves each event by the account ID in the signed payload, so an app subscribed to a different account delivers events that no connection claims.
Meta signs every POST with `X-Hub-Signature-256`, and Artemis computes the HMAC over the exact raw bytes.
Any proxy or middleware in front of Artemis has to pass the body through untouched and keep the header. Parsing and reserializing the JSON changes the digest even when the content stays identical, which produces a signature failure that looks like a credential problem.
***
## Verify the connection
Artemis doesn't synthesize Meta events, so a real Instagram conversation is the only end-to-end proof.
1. From an account holding an app or Instagram tester role, send the professional account a short direct message you can search for later.
2. Confirm Meta reports a successful delivery and that Artemis opens one session.
Artemis ignores echo events, so your own outbound messages shouldn't create extra turns.
3. Check that a single reply reaches the original sender in the same conversation.
A send failure here usually means the Page Access Token is wrong or expired, the account linkage broke, the messaging permission is missing, or the conversation has fallen outside the messaging window.
4. Trigger an authored quick reply and a template action, and confirm each resumes the same session rather than starting a new one.
5. Send an image, video, audio file, and document, and confirm the agent handles the downloaded attachment.
Artemis downloads signed Meta CDN URLs promptly because they expire, so a delay in your own processing isn't the same as a delivery failure.
6. Once Meta approves the app and you switch it to Live, repeat the test from an account with no role. This is the only check that proves ordinary people can reach the agent.
***
## Instagram channel behavior and limits
Meta enforces these boundaries, and Artemis applies its own download policy on inbound media. Design your agent's responses to fit them.
| Area | Limit |
| - | - |
| Quick replies | Buttons and selects become quick replies, capped at 13 options with visible titles truncated to 20 characters. |
| Carousels | Generic-template rich content supports at most 10 elements, each carrying at most 3 buttons. |
| Unsupported inputs | Artemis ignores share, story mention, like-heart, reel, sticker, and fallback-only attachments rather than turning them into blank agent turns. |
| Media downloads | Image, video, audio, and file downloads accept only recognized Meta and Instagram CDN hosts, time out after 30 seconds, and cap at 100 MB streaming. |
| Messaging window | Meta can reject outbound delivery outside the Instagram messaging window, which runs 24 hours from the person's message. A human agent tag extends that to 7 days for approved use cases. |
| Rate limits | Meta allows roughly 100 calls per second per account for text messages, and roughly 10 per second for audio or video. |
***
## Secure the connection
Instagram authentication runs in one direction only, which is easy to misread. Meta signs what it sends you; your Verify Token does nothing after setup.
Every POST requires `X-Hub-Signature-256` generated with the matching App Secret, and Artemis rejects a missing or invalid signature before it queues anything. Signature validation covers the exact raw bytes, so preserve the request body, its content encoding, the header, and the external scheme through your ingress.
Keep the three secrets distinct in your own operational notes. The Verify Token protects only the one-time challenge, and you rotate it in both Artemis and Meta together. The App Secret authenticates inbound traffic. The Page Access Token authorizes outbound messages, so encrypt it, redact it from logs, grant least privilege, monitor its expiry, and never expose it to browser code.
Use separate Meta apps and accounts for development and production, or at minimum isolated credentials, tokens, webhook subscriptions, and connections.
***
## Manage the channel connection
Opening a connection shows four tabs. The delete control sits beside the connection name.
**Connection Summary** reports the connection's state and identity.
| Field | Description |
| - | - |
| **Status** | Whether the connection currently handles messages. |
| **Created** and **Updated** | Date and time of creation, and of the last change. |
| **Environment** | The environment whose deployment answers messages. |
| **Source Type** | Reads `Channel Connection`. |
| **Source ID** | Identifies the connection uniquely. Quote it when you raise a support ticket. |
**Setup Instructions** shows the **Webhook URL** to register in Meta and the **Instagram Account ID** this connection matches events to, each with a copy control. Check the account ID here first when messages arrive and no session appears.
Artemis encrypts credentials after you save them, so each field reads as saved rather than showing its value. Leave a field empty to keep the current value, or enter a new one to replace it.
| Field | Description |
| - | - |
| **Use Auth Profile** | Draws the credentials from a saved auth profile instead of the fields below. Prefer it when the same credentials serve more than one connection, since a rotation then touches one profile. |
| **Page Access Token** | Replace it here when you generate a new token in Meta. The account ID stays the same, so you don't need a new connection. |
| **App Secret** | Replace it here if you move the Instagram product to a different Meta app. |
| **Verify Token** | Changing this breaks Meta's webhook until you enter the matching value in Meta as well. |
Click **Save Credentials** to apply your changes.
Under **General**, rename the connection with **Display Name**, and revisit **Provider Verification Strength** if your trust posture for this account changes.
Under **Instagram Settings**, **Instagram Account ID** appears read-only. Artemis routes inbound events by this value, so changing it would orphan the connection. Create a new connection to serve a different account.
**Authorization Message** controls what the person sees when a tool needs them to authorize something before the conversation continues. Write the template with these variables.
| Scope | Variables | Use |
| - | - | - |
| Message | `{{total}}`, `{{completed}}`, `{{remaining}}` | Summarize progress across every pending authorization, typically in a lead-in line above the list. |
| Repeat block | `{{#authorizations}}` and `{{/authorizations}}` | Wrap the section Artemis repeats once per pending authorization. |
| Item | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Identify and link each authorization inside the repeat block. |
Item variables resolve only inside the repeat block. Use the preview to check the wording, and remember it fills from live pending authorizations at runtime.
Click **Save Changes** to apply your changes.
Choose which deployed version of the agent answers messages on this connection.
Under **Agent Version**, set **Environment** to the environment you want the connection to follow. **Active Deployment** then shows the version currently serving traffic and when someone deployed it.
Check this pair when the channel connects but the agent behaves like an older build. The connection follows whatever the environment currently has deployed, not the version you last edited. Click **Save Changes** after you switch the environment.
***
## Troubleshoot connection issues
| Symptom | Likely cause | Recommended action |
| - | - | - |
| Meta can't verify the callback | The connection doesn't exist yet, the tokens differ, or the endpoint isn't publicly reachable | Create the connection and set it to Active first, use the exact saved Verify Token in both systems, expose the runtime over public HTTPS, and keep the generic Instagram webhook path. |
| Inbound requests return 401 | The App Secret belongs to a different app, or ingress altered the signed request | Confirm the App Secret comes from the app that owns the webhook, preserve `X-Hub-Signature-256`, and stop proxies from parsing and reserializing the JSON body. |
| Artemis reports no connection found | The account ID in the payload doesn't match the saved value | Compare the account ID in the signed payload with the **Instagram Account ID** saved in Artemis, and confirm the connection is Active. A Page ID or an IGSID in this field produces exactly this result. |
| Verification succeeded but no messages arrive | The subscription is missing, or the sender isn't eligible in the current app mode | Subscribe `messages` and `messaging_postbacks`, connect the intended professional account, inspect Meta's delivery log, and confirm the sender holds a role while the app is in Development. |
| Inbound works but outbound fails | The Page Access Token is wrong, expired, or under-permissioned, or the window closed | Verify the token, the account linkage, the Instagram messaging permission, the token expiry, the messaging window, and reachability of the Graph API. |
| Text works but actions don't | `messaging_postbacks` isn't subscribed, or the payload exceeds channel limits | Subscribe the field, then keep authored quick replies and template payloads within the limits above. |
| Text works but attachments fail | The media type, host, size, or signed URL falls outside policy | Use image, video, audio, or file, confirm the signed URL hasn't expired, confirm the host is a recognized Meta CDN, keep the download within 100 MB, and confirm your attachment policy accepts it. |
| Meta rejects replies after a delay | The conversation fell outside the messaging window | Reproduce inside an active conversation, then review Meta's current Instagram messaging window and approved use cases. |
***
## Meta reference
Use these Meta pages when you need detail beyond what this article covers.
| Resource | Use it for |
| - | - |
| [Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview/) | Account eligibility, how the pieces fit together, and rate limits. |
| [Instagram messaging get started](https://developers.facebook.com/docs/messenger-platform/instagram/get-started) | Meta's own walkthrough of app setup, permissions, and Page token generation. |
| [Instagram messaging overview](https://developers.facebook.com/docs/instagram-messaging/overview) | Conversation entry points, supported message types, and the inbox model. |
| [Send a message](https://developers.facebook.com/docs/messenger-platform/instagram/features/send-message) | Outbound message formats, media limits, and the messaging window rules. |
| [My Apps](https://developers.facebook.com/apps/) | Returning to an app you created, or checking which app owns a subscription. |
| [Meta Business Suite](https://business.facebook.com/) | Page roles, linked accounts, and business verification. |
**Related articles:**
* [Set up Messenger](/agent-platform/channels/messenger)
* [Set up WhatsApp](/agent-platform/channels/whatsapp)
* [Set up LINE](/agent-platform/channels/line)
# Kore Agent Assist
Source: https://koreai.mintlify.app/agent-platform/channels/kore-agent-assist
The Kore Agent Assist channel connects an Artemis project to Kore Agent AI, so your deployed agents give contact center agents real-time guidance. Artemis creates the connection, generates its API key, binds it to an environment, and controls traffic through **Pause** and **Resume**. Kore Agent AI keeps calling the existing Agent Assist API, so the external endpoint contract doesn't change.
The setup requires:
1. [Create the connection in Artemis](#create-the-connection-in-artemis)
2. [Configure Kore Agent AI](#configure-kore-agent-ai)
3. [Verify the connection](#verify-the-connection)
After setup, Kore Agent AI routes each Agent Assist session to the agent version active in the environment you select.
**Important considerations**
* **The API key appears only once.** Artemis shows the full key only in the creation dialog. Afterward, only the key prefix stays visible. Copy the key before you select **Done**.
* **Pause and Resume control traffic.** The connection has no separate on or off setting. A paused or inactive connection returns `404` to external requests.
* **Use one connection per environment.** Keep development, staging, and production on separate connections, each with its own key.
* **The key has a narrow scope.** It covers message sending for the owning project only and doesn't grant platform administration access.
* **Output supports text and markdown.** Responses can also carry normalized renderables, actions, and activity events. The channel doesn't accept attachments or deliver channel-native rich media.
## Prerequisites
Confirm the following before you start.
* An Artemis project with an agent version deployed to the target environment. The connection's **Deployment** tab lists it as the **Active Deployment**.
* Access to **Deployments** in the Artemis project.
* Administrator access to Kore Agent AI, including the **Configuration** section.
* A secure location, such as a secrets manager, to store the API key.
***
## Create the connection in Artemis
Create the connection first. Artemis generates the four values that Kore Agent AI needs.
1. In your project, go to **Deployments** -> **Channels**.
2. Select **Kore Agent Assist**.
3. Select **Create Connection**. If the project already has connections, select **New**.
1. In **Display Name**, enter a name that identifies the Agent Assist experience and its environment. The name supports up to 255 characters.
2. In **Environment**, select the environment whose active deployment supplies the entry agent, welcome message, model, and runtime behavior.
3. In **Initial Status**, select **Active** to accept traffic immediately, or select **Inactive** to save the connection and activate it later.
4. Select **Create**.
The **Kore Agent Assist Connection Created** dialog shows four values.
1. Copy **Domain URL**, **Environment**, **App ID**, and **API Key**.
2. Store the API key in your secrets manager.
3. Select **Done**.
After you close the dialog, Artemis can't show the full API key again. If you lose the key, rotate it from the **Overview** tab.
### Connection values
Each value maps to a field of the same name in Kore Agent AI. Copy each one exactly, including case and punctuation.
| Value | Source / Notes |
| - | - |
| **Domain URL** | The Artemis host that Kore Agent AI calls. Agent AI appends the Agent Assist API route, so don't paste an individual endpoint path. |
| **Environment** | The environment identifier. It can differ from the label in the **Environment** list, so use the value from the dialog. |
| **App ID** | The project ID for the connection. Don't substitute the display name, an agent name, or the **Source ID** from the **Overview** tab. |
| **API Key** | A generated secret with the `abl_` prefix. Agent AI sends it in the `x-api-key` header. Keep it out of browser code, transcripts, client logs, and source control. |
***
## Configure Kore Agent AI
Add the connection as an Agentic Copilot app in Kore Agent AI. For the complete Agent AI procedure, see [Linked Services](https://docs.kore.ai/agentassist/settings/linked-services/).
1. Sign in to [Kore Agent AI](https://agentassist.kore.ai/koreagentassist/).
2. Go to **Configuration** -> **System Setup** -> **Linked Services**.
3. Select **Agentic Configurations**.
4. Select **+ Add Agentic Copilot App**.
Under **App Information**, enter the values from the Artemis connection.
1. In **App Name**, enter a name that agents and administrators recognize. The name supports up to 50 characters.
2. In **Domain URL**, paste the Artemis **Domain URL**.
3. In **Environment**, paste the Artemis **Environment** value.
4. In **App ID**, paste the Artemis **App ID**.
5. In **API Key**, paste the full API key, not the prefix.
Select **Test Connection & Save**. Agent AI checks the values and saves the app when the connection succeeds.
Next, link the app to your Agent AI experience so agents receive guidance at runtime. For details, see [Agentic Copilot](https://docs.kore.ai/agentassist/uncategorized/agentic-copilot/).
### Validation checklist
Review these items before you route live conversations to the connection.
* The Artemis connection shows **Active** status.
* The selected environment has an **Active Deployment**.
* Agent AI holds the exact **Domain URL**, **Environment**, and **App ID** values.
* Agent AI holds the current, full API key.
* **Test Connection & Save** succeeds in Agent AI.
* Every action on the Artemis **Testing** tab returns a successful result.
***
## Verify the connection
The **Testing** tab calls the live Agent Assist API with the connection's saved values. Run it before you add the values to Agent AI, and again after any key rotation.
1. Open the connection, and select the **Testing** tab.
2. Under **Connection**, confirm the **Domain URL**, **Environment**, and **App ID**.
3. In **API Key**, paste the full key you stored at creation. The test actions stay unavailable until you paste a key.
Artemis generates a **sessionReference** for the test session. Run the actions in order so every call uses the same session.
1. Select **Test connection** to check authentication and deployment readiness.
2. Select **Connect (welcome)** to open the session and receive the agent's welcome message.
3. In **Message**, enter a test message, and select **Send message**.
4. Turn on **Stream response (SSE)**, and send another message to check streaming delivery.
5. Select **Terminate** to end the session.
**Results** lists the request, status, duration, and response for each action. To start over with a new session reference, select **New session**.
### Test actions
Each test action calls one Agent Assist API endpoint.
| Action | Endpoint | What it confirms |
| - | - | - |
| **Test connection** | `/test` | The key authenticates, the App ID and environment resolve, and the deployment, entry agent, and model are ready. The call doesn't create a conversation or call the model. |
| **Connect (welcome)** | `/sessions` | The API creates the session and returns the authored welcome message. |
| **Send message** | `/runs/execute` | The agent processes a turn and returns a response. With **Stream response (SSE)** on, the response arrives as a Server-Sent Events stream. |
| **Terminate** | `/sessions/terminate` | The API ends the session and returns a termination response. |
### Verify pause and resume
Confirm that the connection status controls all external traffic.
1. In the connection header, select **Pause**.
2. On the **Testing** tab, select **Test connection**, and confirm the request returns `404`.
3. Select **Resume**, and run **Test connection** again.
***
## Manage the connection
Open a connection from **Deployments** -> **Channels** -> **Kore Agent Assist**. The header shows the display name, environment, and status, plus the **Pause** and delete controls.
| Tab | Use it to |
| - | - |
| **Overview** | Review the **Connection Summary**, copy the Agent AI values, and rotate the API key. |
| **Configuration** | Change the **Display Name**, and then select **Save Changes**. |
| **Deployment** | Change the environment that handles incoming messages, and review the **Active Deployment** version and deployment time. |
| **Testing** | Run test actions through the live API. |
### Connection summary
The **Overview** tab lists the connection's metadata under **Connection Summary**.
| Field | Description |
| - | - |
| **Status** | Whether the connection accepts external traffic. |
| **Created** | When you created the connection. |
| **Updated** | When the connection last changed. |
| **Environment** | The environment identifier the connection follows. |
| **Source Type** | The record type. For this channel, the value is **Channel Connection**. |
| **Source ID** | The internal ID of the connection record. Don't use it as the App ID. |
### Change the environment
Changing the environment routes incoming messages to that environment's active deployment.
1. Select the **Deployment** tab.
2. In **Environment**, select the environment.
3. Select **Save Changes**.
4. On the **Overview** tab, check the **Environment** value, and update Agent AI if it differs.
### Rotate the API key
Rotate the key when you lose it or suspect exposure.
1. On the **Overview** tab, select **Rotate** next to **API Key**.
2. Copy the new key. Artemis shows it only once.
3. In Agent AI, replace the **API Key** value, and select **Test Connection & Save**.
Artemis revokes the previous key when it saves the replacement. Agent AI requests fail until you update the key.
### Pause, resume, or delete
Use the header controls to change the connection's availability.
* Select **Pause** to stop external traffic. Requests return `404` until you select **Resume**.
* Select the delete icon to remove the connection. Before you delete it, remove or update the matching app in Agent AI.
***
## API reference
Kore Agent AI calls these endpoints on your behalf. Use this section to build a custom integration or to diagnose failed requests.
### Base path and authentication
All endpoints share one base path built from the connection values.
```text theme={null}
{Domain URL}/api/v2/apps/{App ID}/environments/{Environment}
```
Every request sends `Content-Type: application/json` and the full API key in the `x-api-key` header. Don't send an `Authorization: Bearer` header, a user token, an SDK public key, or the key prefix. The API checks that the key's tenant and project match the resolved App ID and environment, and returns `404` on any mismatch.
### Session lifecycle
Each Agent Assist conversation uses the same session identity across all calls.
| Step | Endpoint | Details |
| - | - | - |
| Start | `POST /sessions` | Send `sessionIdentity` before the first turn. Set `metadata.isSendWelcomeMessage` to `true` to receive the welcome message. |
| Send turns | `POST /runs/execute` | Send `sessionIdentity` and `input[]`. Reuse the same `sessionReference` for later turns. |
| End | `POST /sessions/terminate` | Send the same session identity when the conversation ends. The API returns a termination response even if the session no longer exists. |
| Check readiness | `/test` | Checks authentication and deployment readiness without creating a conversation or calling the model. |
### Response modes
`/runs/execute` supports three response modes. The request fields decide which mode applies.
| Mode | How to request it | Behavior |
| - | - | - |
| Synchronous (default) | Leave streaming off, and omit `callbackUrl`. | Returns one completed response on the same HTTP connection. |
| Streaming (SSE) | Set `stream.enable` to `true`, and accept `text/event-stream`. | Streams the response with heartbeats, then sends the final session and run details. Streaming takes precedence over `isAsync`. |
| Async push | Set `isAsync` to `true`, and include `callbackUrl`. | Validates the callback, returns `202` with a processing state, and later delivers the completed or error response to the callback. |
If you set `isAsync` to `true` without a `callbackUrl`, the API falls back to the synchronous response.
For async push, use an approved HTTPS destination, and avoid private and link-local addresses. When callback signing is on, verify the `X-ABL-Signature` header, which signs `timestamp.rawBody`.
### Payload rules
The API normalizes and restricts request content before agent execution.
| Element | Rule |
| - | - |
| `sessionIdentity` | Accepts `sessionReference`, `sessionId`, `sessionIdentity`, and `userReference` entries. Include at least one stable `sessionReference`, `sessionId`, or `userReference`, and keep it unchanged across calls. |
| `input[]` | Requires at least one item and supports `text`, `object`, and `tool_input` types. Agent execution needs a `text` item. Each content value allows up to 16,000 characters. |
| `metadata.aa_uamsgs` | Carries Agent Assist conversation history. The API keeps up to 50 messages. |
| Other `metadata` | The API accepts custom keys but strips reserved transport, credential, tenant, project, session, and authorization keys. |
| Output | Text and markdown, plus optional normalized renderables, actions, and activity events. No attachments or channel-native rich media. |
***
## Troubleshoot connection issues
Match the symptom to its likely cause, and apply the recommended action.
| Symptom | Likely cause | Recommended action |
| - | - | - |
| `401 Unauthorized` | The request carries a missing, partial, or revoked key. | Paste the full current `abl_` key, and confirm every request sends `x-api-key`. |
| `404` app not found | The App ID or Environment doesn't match, the key belongs to another project, or the connection is paused or inactive. | Copy the values again from the **Overview** tab, and confirm the connection shows **Active**. |
| **Test Connection & Save** fails in Agent AI | One or more pasted values differ from the Artemis connection. | Recopy all four values with the copy icons on the **Overview** tab. |
| `/test` reports a deployment resolution failure | The environment has no active deployment, or its entry agent or model configuration isn't valid. | Deploy an agent version to the environment, and check the entry agent and model settings. |
| Streaming returns JSON or stalls | The request doesn't set `stream.enable`, doesn't accept `text/event-stream`, or a proxy buffers the stream. | Set `stream.enable` to `true`, accept `text/event-stream`, turn off proxy buffering, and let heartbeats pass for the full request timeout. |
| Async returns an invalid callback error or never delivers | The callback URL isn't an approved HTTPS destination, or signature checks fail. | Use an approved HTTPS URL outside private and link-local ranges, and verify `X-ABL-Signature` when signing is on. |
| Welcome and follow-up turns land in different sessions | The session identity changes between calls. | Reuse the same `sessionReference`, and keep identity values and their order unchanged between `/sessions` and `/runs/execute`. |
| Test actions stay unavailable | The **Testing** tab has no API key. | Paste the full key, or rotate it from the **Overview** tab and paste the new one. |
## Related topics
Use these pages to continue channel and deployment setup.
* [Channels](/agent-platform/channels)
* [Deployment](/agent-platform/deployment)
* [Linked Services in Kore Agent AI](https://docs.kore.ai/agentassist/settings/linked-services/)
# Set up LINE
Source: https://koreai.mintlify.app/agent-platform/channels/line
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital)
Connect a LINE Official Account to your agent so people reach it in LINE for chat and media messaging. You create the Official Account, enable the Messaging API on it, collect two credentials and one identifier, then register a webhook and verify it before any real traffic arrives.
The setup requires:
1. [LINE setup](#line-setup): Create the Official Account, enable the Messaging API, and collect the Channel Secret, Channel Access Token, and Destination ID.
2. [Artemis channel configuration](#artemis-channel-configuration) using those three values, then copy the generated webhook URL.
3. [Webhook configuration in LINE](#webhook-configuration-in-line): Register the webhook URL, verify it, and switch delivery on.
After the configuration is complete, LINE posts every event for the channel to Artemis, which resolves the connection from the payload and hands the message to the deployed agent.
**Important considerations**
* **Two consoles, two halves of the job**: LINE splits the work between LINE Official Account Manager and the LINE Developers Console. Sign in to both with the same account. The Developers Console asks first-time users to register a developer profile with a name and email, and an account that skips that step can't see the channel it just created.
* **Official Account Manager creates the channel, not the Developers Console**: LINE removed the ability to create Messaging API channels directly in the Developers Console in September 2024. Enabling the Messaging API on the Official Account creates the channel.
* **Provider selection is permanent**: You can't change or remove the provider afterwards. A person also receives a different user ID under each provider, so moving an account later would break every stored user reference.
* **The Destination ID is not shown in either console**: It comes from an API call. Three identifiers that are shown look plausible and none of them route. See [How Artemis routes LINE events](#how-artemis-routes-line-events).
| Console | What you do there |
| - | - |
| [LINE Official Account Manager](https://manager.line.biz/) | Create the Official Account, enable the Messaging API, and turn off LINE's automatic replies. |
| [LINE Developers Console](https://developers.line.biz/console/) | Copy the Channel Secret and access token, set the webhook URL, and find the friend-add QR code. |
## Prerequisites
* A LINE Business ID, registered with an existing personal LINE account or with an email address. See [LINE setup](#line-setup).
* The admin role on the Official Account and on the Messaging API channel. Several steps hide their controls from lower roles rather than reporting a permission error.
* The admin role on the provider you intend to use, if you plan to select an existing one. A provider only appears in the selection list when your account holds that role on it.
* Artemis project access with permission to open **Deployments** -> **Channels**, and a deployed agent in the environment you intend to bind.
***
## How Artemis routes LINE events
LINE sends every event for a channel to one URL, so Artemis identifies the connection from the payload rather than the path. Each webhook body carries a `destination` field holding the bot's user ID, and Artemis resolves the connection from that value before it verifies the signature.
| Piece | Who owns it | What it does |
| - | - | - |
| **Destination ID** | LINE | Routes inbound events. It has to be the bot's user ID, which begins with `U`. |
| **Channel Secret** | LINE | Verifies the `X-Line-Signature` header. Artemis computes HMAC-SHA256 over the unchanged request body and compares the result. |
| **Channel Access Token** | LINE | Authorizes outbound calls: replies, push messages, bot info lookups, and the loading indicator. |
| **Webhook URL** | Artemis | Receives events. The route carries no Destination ID, because the destination travels in the signed body. |
Three LINE identifiers look plausible in the Destination ID field and none of them route. The **Channel ID** on the Basic settings tab is numeric. The **Bot basic ID** on the Messaging API tab starts with `@`. **Your user ID**, also on the Basic settings tab, is your own personal LINE account, not the bot's. The correct value comes from an API call, covered in [LINE setup](#line-setup).
***
## LINE setup
Complete all of this in LINE before you open Artemis. Start at the beginning even if someone has already created an Official Account, because enabling the Messaging API is what creates the channel you'll configure.
LINE accounts for business sit behind a Business ID, which you can register with an existing personal LINE account or with an email address.
1. Go to [account.line.biz](https://account.line.biz/signup?redirectUri=https://entry.line.biz/form/entry/unverified) and register a Business ID, if your organization doesn't already have one.
2. Use an account your team can keep. This identity carries the admin role for everything that follows.
1. Complete LINE's [Official Account entry form](https://entry.line.biz/form/entry/unverified) with your business details. Submitting it creates the account.
2. Open [LINE Official Account Manager](https://manager.line.biz/) and confirm the new account appears in your list before you continue.
1. In Official Account Manager, select the account.
2. Go to **Settings** -> **Messaging API** and enable the Messaging API. This is what creates the channel that Artemis connects to.
You can't do this anywhere else. LINE removed the ability to create Messaging API channels directly in the Developers Console in September 2024. Starting from the developer side finds nothing to create.
LINE asks you to pick a provider, which is the organization that owns the channel. Two constraints make this a decision rather than a formality.
You can't change or remove the provider afterwards. A person also receives a different user ID under each provider, so moving an account later would break every stored user reference. An existing provider only appears in the list when your account holds the admin role on it. A provider that seems to be missing is usually a permissions problem, not an absent one.
Pick the provider that owns your related channels, and avoid putting unrelated services under one provider for convenience.
A new channel arrives with greeting messages and auto-reply messages enabled, which means a person gets one LINE-managed response and one agent response to the same message.
Reach the settings from either console:
1. In Official Account Manager, open **Settings** -> **Response settings**.
2. Or in the Developers Console, open the channel's **Messaging API** tab, where **Greeting messages** and **Auto-reply messages** each carry an **Edit** link that opens Official Account Manager at the right place.
LINE recommends disabling both for a first Messaging API bot, and explains why under [settings on LINE Official Account Manager](https://developers.line.biz/en/docs/messaging-api/building-bot/#line-manager-settings). Disable both when the agent should own every reply. Keeping a greeting message is reasonable if you author it deliberately, but leaving the defaults on reads to users as a broken bot rather than as a settings problem.
All three credential values live in the Developers Console, spread across two tabs of the same channel.
1. Sign in at [developers.line.biz/console](https://developers.line.biz/console/).
2. Select your provider, then select the Messaging API channel.
3. Open the **Basic settings** tab and copy **Channel secret**. You need the admin role on the channel to see it.
Take care not to grab the **Channel ID** or **Your user ID** from the same screen. Both sit nearby, both look like credentials, and neither works.
Open the **Messaging API** tab. LINE offers four token types, and this screen issues only one of them.
| Token type | Validity | Limit per channel |
| - | - | - |
| User-specified expiration (v2.1) | Up to 30 days | 30 |
| Stateless | 15 minutes | No limit |
| Short-lived | 30 days | 30 |
| Long-lived | Until you revoke it | 1 |
The long-lived token is the one with an **Issue** control on this tab. The other three come from API calls rather than the console, so choosing them means building issuance into your own tooling.
LINE recommends the v2.1 token, and that recommendation costs you a rotation schedule. Pick the long-lived token if you'd rather not build that, and accept that a single indefinite credential carries more risk. Whichever you choose, copy the value straight into your secret manager.
Reissuing a long-lived token normally kills the previous one immediately. LINE lets you extend the outgoing token's validity by up to 24 hours when you reissue, which gives you a window to update Artemis without dropping messages. Use it.
The console never shows this value, so you call for it.
1. Use LINE's [Get bot info](https://developers.line.biz/en/reference/messaging-api/#get-bot-info) endpoint with the channel access token you just issued.
```bash theme={null}
curl -v -X GET https://api.line.me/v2/bot/info \
-H "Authorization: Bearer {channel access token}"
```
2. Copy the `userId` from the response.
The value begins with `U` and matches the `destination` field LINE puts in every webhook for this channel. Run this from a secure terminal rather than a shared one, since the command carries your token.
While you're on the **Messaging API** tab, find the QR code or add-friend link. You'll need it to test, and it's easier to grab now than to come back for.
***
## Artemis channel configuration
Create the connection before you verify the webhook in LINE. LINE's verification request has to resolve to an active connection and authenticate with that connection's Channel Secret, so an unsaved connection fails verification.
1. Open Artemis and select the project.
2. Go to **Deployments** -> **Channels** -> **LINE** -> **+ New**. The **New LINE Connection** dialog opens.
3. Configure the following fields:
| Artemis connection field | Source / Notes | What it controls |
| - | - | - |
| Display Name | You choose it | The label operators see in the connections list. Name it for the Official Account and the environment it serves, because a project can hold several LINE connections. Routing ignores this value. |
| Destination ID | The `userId` from Get bot info. See [LINE setup](#line-setup). | The routing key. It begins with `U`. |
| Channel Access Token | Developers Console > channel > **Messaging API** tab | The credential for every outbound call Artemis makes to LINE. |
| Channel Secret | Developers Console > channel > **Basic settings** tab | The key Artemis uses to verify the signature on each inbound event. |
| Environment | Your published environments | The environment whose active deployment answers incoming messages. Leave the working copy default only while you develop. |
| Provider Verification Strength | Defaults to **Weak** | How far Artemis trusts LINE's identity assertion when it links a person to an existing session. **Weak** suits most deployments. Choose **Strong** only when you trust those assertions enough to base continuity and account-linking decisions on them. |
| Initial Status | Defaults to **Active** | Whether the connection starts handling messages. Set it to **Active**, because LINE's verification resolves only when the connection is already active. |
4. Click **Create**.
5. Open the connection and copy the **Webhook URL** from the **Overview** tab. You need it for the next section.
***
## Webhook configuration in LINE
LINE separates saving the URL from switching delivery on, so a verified webhook can still deliver nothing. Complete all three parts in the Developers Console, on the channel's **Messaging API** tab.
1. Find **Webhook settings**.
2. Click **Edit** under **Webhook URL**.
3. Paste the Artemis endpoint exactly as shown and click **Update**.
LINE accepts one endpoint per channel. Don't append the Destination ID, the Channel ID, a project ID, or any connection identifier, because Artemis routes from the signed body rather than the path.
LINE also requires HTTPS with a certificate from a widely trusted authority. Self-signed certificates fail, and so does a server missing its intermediate certificates, which is a common cause of a verification failure that looks like a LINE problem.
Click **Verify**. LINE posts an event-free payload to the endpoint, and Artemis responds once it has resolved the connection and verified the signature. LINE reports **Success** when this works.
Treat anything else as a stop. A timeout, a non-200 response, a TLS error, and a signature failure each point somewhere different, and continuing past one means debugging later with live users watching.
Turn on **Use webhook**.
This is the step people miss. Saving a verified URL without this toggle leaves the channel looking correctly configured while message events never reach Artemis.
Enable redelivery only when your operations policy requires retries after a non-2xx response.
LINE can deliver the same webhook more than once when redelivery is on, and it can change event order during redelivery. Your agent logic has to tolerate both, so rely on event IDs rather than arrival order before you turn this on.
The channel's **Security** tab accepts a list of IP addresses, individually or in CIDR notation, that may call the LINE Platform with a long-lived token.
This narrows the blast radius of an indefinite credential considerably, so it's worth doing whenever you pick that token type. LINE covers the setting under [restrict who can call the API](https://developers.line.biz/en/docs/messaging-api/building-bot/#configure-security-settings). It doesn't apply to the other three.
### Validation checklist
| Validation item | Expected result |
| :- | :- |
| Complete LINE setup | Official Account created, Messaging API enabled, and greeting and auto-reply messages disabled. |
| Collect credentials | Channel Secret, Channel Access Token, and the `U`-prefixed Destination ID copied from the matching channel. |
| Create the Artemis connection | Connection saved as **Active** with the Destination ID, both credentials, and the intended environment. |
| Register the webhook URL | LINE reports **Success** when you click **Verify**. |
| Enable delivery | **Use webhook** shows as turned on. |
| Send a live message | A real LINE conversation produces exactly one agent reply. See [Verify the connection](#verify-the-connection). |
## Verify the connection
Artemis has no synthetic test for this channel, so a real conversation is the only end-to-end proof.
1. Add the Official Account as a friend by scanning the QR code or using the add-friend link on the **Messaging API** tab. Test from an ordinary LINE account rather than an identity that exists only as a console administrator, because the two behave differently.
2. Send a short message you can search for later, and confirm the agent replies in the same chat.
3. Count the replies. Two responses to one message means greeting messages or auto-reply messages are still enabled, not that the agent ran twice.
4. Send each input type your agent supports, and confirm its authored behavior for each. Receiving a media event isn't the same as handling it, so test the authored path rather than only the delivery.
5. Test groups only if you need them. Enable **Allow bot to join group chats** in Official Account Manager, invite the bot, then message or mention it. Confirm sessions stay scoped to the LINE source, so a group conversation and a one-to-one conversation with the same person don't merge.
***
## Secure the connection
Both LINE credentials are secrets, and they do opposite jobs. The Channel Secret authenticates what comes in; the Channel Access Token authorizes what goes out.
Artemis computes HMAC-SHA256 over the unchanged raw request body and compares it with the `X-Line-Signature` header. Any proxy or middleware in front of Artemis has to leave the signed body untouched, because a single changed byte fails verification as surely as a wrong secret does.
Rotation invalidates immediately on both credentials. Reissuing a Channel Secret kills the old secret, and reissuing a long-lived access token kills the previous token unless you use the 24-hour extension. Coordinate the Artemis update with the provider-side change rather than doing them hours apart. Revoke any token you suspect someone has copied, because a compromised token can broadcast to every friend of the Official Account.
Keep both values out of browser code, screenshots, tickets, and source control, and update Artemis the moment LINE reissues or revokes either one.
Use distinct providers or channels for development and production, or at minimum distinct credentials and connections. Pointing two environments at one webhook mixes test traffic into real conversation history.
***
## Manage the channel connection
Opening a connection shows four tabs. The delete control sits beside the connection name.
**Connection Summary** reports the connection's state and identity.
| Field | Description |
| - | - |
| **Status** | Whether the connection currently handles messages. |
| **Created** and **Updated** | Date and time of creation, and of the last change. |
| **Environment** | The environment whose deployment answers messages. |
| **Source Type** | Reads `Channel Connection`. |
| **Source ID** | Identifies the connection uniquely. Quote it when you raise a support ticket. |
**Setup Instructions** shows the **Webhook URL** to register in the Developers Console and the **Destination ID** this connection matches events to, each with a copy control. Check the Destination ID here first when messages arrive and no session appears.
Artemis encrypts credentials after you save them, so each field reads as saved rather than showing its value. Leave a field empty to keep the current value, or enter a new one to replace it.
| Field | Description |
| - | - |
| **Use Auth Profile** | Draws the credentials from a saved auth profile instead of the fields below. Prefer it when the same credentials serve more than one connection, since a rotation then touches one profile. |
| **Channel Access Token** | Replace it here whenever you reissue the token in the Developers Console, and do it promptly, because the old token stops working at once. |
| **Channel Secret** | Replace it here if another administrator reissues the secret. Until you do, every inbound event fails signature verification. |
Click **Save Credentials** to apply your changes.
Under **General**, rename the connection with **Display Name**, and revisit **Provider Verification Strength** if your trust posture for this Official Account changes.
Under **LINE Settings**, **Destination ID** appears read-only. Artemis routes inbound events by this value, so changing it would orphan the connection. Create a new connection to serve a different Official Account.
**Authorization Message** controls what the person sees when a tool needs them to authorize something before the conversation continues. Write the template with these variables.
| Scope | Variables | Use |
| - | - | - |
| Message | `{{total}}`, `{{completed}}`, `{{remaining}}` | Summarize progress across every pending authorization, typically in a lead-in line above the list. |
| Repeat block | `{{#authorizations}}` and `{{/authorizations}}` | Wrap the section Artemis repeats once per pending authorization. |
| Item | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Identify and link each authorization inside the repeat block. |
Item variables resolve only inside the repeat block. Use the preview to check the wording, and remember it fills from live pending authorizations at runtime.
Click **Save Changes** to apply your changes.
Choose which deployed version of the agent answers messages on this connection.
Under **Agent Version**, set **Environment** to the environment you want the connection to follow. **Active Deployment** then shows the version currently serving traffic and when someone deployed it.
Check this pair when the channel connects but the agent behaves like an older build. The connection follows whatever the environment currently has deployed, not the version you last edited. Click **Save Changes** after you switch the environment.
***
## Troubleshoot connection issues
| Symptom | Likely cause | Recommended action |
| - | - | - |
| Verify doesn't report success | The connection isn't active, the endpoint isn't reachable, or the certificate chain is incomplete | Confirm the connection exists and is active, that the Artemis endpoint is reachable over public HTTPS with a certificate from a trusted authority and a complete chain, that the URL carries no extra path segment, and that LINE receives a 200 response. |
| Artemis reports no connection found | The Destination ID holds the wrong identifier | Replace it with the `U`-prefixed `userId` from Get bot info. The Channel ID, the Bot basic ID, and personal user IDs don't route. |
| Signature verification fails | The Channel Secret doesn't match, or a proxy rewrote the body | Copy the Channel Secret from the matching channel's **Basic settings**, check whether another administrator reissued it, and confirm no proxy or middleware rewrites the raw body. |
| Verification succeeded but no messages arrive | **Use webhook** is off | Turn on **Use webhook**. A saved and verified URL delivers nothing until you do. |
| Messages arrive but the agent doesn't reply | The access token expired, someone revoked it, or no deployment can run | Confirm the Channel Access Token is current, the connection is active, and the bound environment has a runnable deployment. A v2.1 token past its 30-day ceiling produces exactly this symptom. |
| People receive two replies | LINE's own automatic replies are still enabled | Disable greeting messages and auto-reply messages, or author them so they don't overlap with the agent. |
| Direct messages work but groups don't | The bot can't join group chats, or it joined before you changed the setting | Enable **Allow bot to join group chats**, reinvite the bot if you changed the setting after inviting it, and confirm the test event comes from the expected group or room. |
| Text works but media doesn't | The event type, the token, the size limit, or the agent flow blocks it | Confirm Artemis supports the event type, the token can download message content, attachment limits allow the file, and the agent flow handles attachments. |
| The provider you want isn't in the list | Your account lacks the admin role on it | Ask a provider admin to grant the role rather than creating a second provider. |
***
## LINE reference
Use these LINE pages and consoles when you need detail beyond what this article covers.
| Resource | Use it for |
| - | - |
| [LINE Official Account Manager](https://manager.line.biz/) | Creating the Official Account, enabling the Messaging API, response settings, group chat permission, and admin roles. |
| [LINE Developers Console](https://developers.line.biz/console/) | Channel credentials, webhook settings, the security tab, and the friend-add QR code. |
| [Get started with the Messaging API](https://developers.line.biz/en/docs/messaging-api/getting-started/) | LINE's own walkthrough of Business ID registration, account creation, and provider selection. |
| [Build a bot](https://developers.line.biz/en/docs/messaging-api/building-bot/) | Tokens, the webhook URL, verification, and the certificate requirements. |
| [Channel access tokens](https://developers.line.biz/en/docs/basics/channel-access-token/) | How the four token types differ and which suits your rotation policy. |
| [Messaging API reference](https://developers.line.biz/en/reference/messaging-api/) | Endpoint detail, including [Get bot info](https://developers.line.biz/en/reference/messaging-api/#get-bot-info) and webhook event objects. |
**Related articles:**
* [Channels](/agent-platform/channels)
* [Set up Messenger](/agent-platform/channels/messenger)
* [Set up WhatsApp](/agent-platform/channels/whatsapp)
* [Set up Telegram](/agent-platform/channels/telegram)
# Set up Messenger
Source: https://koreai.mintlify.app/agent-platform/channels/messenger
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital)
Connect a Facebook Page to your agent so people reach it through Messenger. You prepare one Meta app and one Page, hand Artemis four values from them, then register a signed webhook that Meta verifies before it sends anything.
The setup requires:
1. [Meta setup](#meta-setup): create a business app, connect the Page, and collect the Page ID, Page Access Token, and App Secret.
2. [Artemis channel configuration](#artemis-channel-configuration) using those values plus a Verify Token you invent, then copy the generated webhook URL.
3. [Webhook configuration in Meta](#webhook-configuration-in-meta): register the callback URL, complete Meta's challenge, and subscribe the Page to the message events.
After you complete the configuration, Meta posts signed Page events to Artemis, which matches the Page ID in the payload to this connection and hands the message to the deployed agent.
**Important considerations**
* **Every value must come from one app and one Page**: The Page ID, Page Access Token, App Secret, Verify Token, and webhook subscription all have to belong to the same Meta app and Page setup. Mixing values from two apps produces failures that look unrelated to each other.
* **One Page routes to one connection**: Reusing a Page means deactivating the existing connection first, or the second one has nothing to route to.
* **Verification and subscription are separate steps**: Meta verifies the callback URL whether or not you subscribe to anything, so a verified webhook with no subscription looks correct on screen and delivers nothing.
* **Development mode blocks the public**: Until Meta approves the app and you switch it to Live, Page tokens reach only accounts holding an app or Page role. A successful test with a role-holding account proves nothing about public access.
The Meta work spans three screens in the App Dashboard at [Meta for Developers](https://developers.facebook.com/).
| Meta screen | What you do there |
| - | - |
| Messenger settings | Connect the Page, generate the Page Access Token, and configure the webhook. |
| **App settings** -> **Basic** | Reveal and copy the App Secret. |
| **Roles** tab | Add the testers who can reach your Page before app review. |
## Prerequisites
* A Meta developer account, and a Facebook Page that customers will message.
* Enough access on the Page to perform the messaging and moderate tasks. Meta requires this of whoever generates the Page Access Token, so an editor or viewer role fails even when the app side looks correct.
* An HTTPS-reachable Artemis runtime, since Meta calls the callback URL during verification.
* Artemis project access with permission to open **Deployments** -> **Channels**, and a deployed agent in the environment you intend to bind.
***
## How Artemis routes Messenger events
Meta sends every Page event to one generic URL, so Artemis identifies the connection from the payload rather than the path. It reads the Page ID out of the signed event and matches it to a connection.
| Piece | Who owns it | What it does |
| - | - | - |
| **Facebook Page ID** | Meta | Routes inbound events. Artemis reads the Page ID from each signed webhook and matches it to this connection. |
| **Page Access Token** | Meta | Authorizes outbound replies through the Messenger Send API. It scopes to one Page, so a token for a different Page fails to send. |
| **App Secret** | Meta | Verifies the `X-Hub-Signature-256` header on every inbound POST. Artemis computes the HMAC over the unchanged request body and rejects anything that doesn't match. |
| **Verify Token** | You | Answers Meta's one-time verification challenge when you save the callback URL. It does nothing after that. |
| **Webhook URL** | Artemis | Receives events. The route ends at the generic Messenger path and carries no Page ID, because the Page ID travels in the payload. |
***
## Meta setup
Complete all of this in Meta before you open Artemis. Meta covers the same ground in its [Messenger Platform quick start](https://developers.facebook.com/docs/messenger-platform/getting-started/quick-start/).
1. Sign in to [Meta for Developers](https://developers.facebook.com/) and open [My Apps](https://developers.facebook.com/apps/).
2. Create an app, or open the one that will own this connection.
3. When Meta asks what the app does, choose the business messaging use case, or choose **Other** and then **Business** as the app type if Meta shows you that older flow.
Only a business app exposes the Messenger product, so choosing the wrong type means creating a second app rather than converting the first.
Use separate apps or Pages for development and production, because app mode and Page subscriptions belong to the app.
1. On the App Dashboard, find **Messenger** in the products list.
2. Click **Set up**. The left menu then carries a Messenger section holding the settings screen you'll return to several times.
Meta renames this screen between app types, so you may see **Messenger API Settings**, or **Messenger** followed by **Settings**. Whichever label appears, the screen you want holds three areas together: a Page connection, a token generation panel, and webhook controls. If the screen you're looking at has only one of those, you're on the wrong one.
1. In the token generation area, add or select the Page customers will message.
2. Authorize the app on that Page when Meta asks.
Your account needs enough access on both sides. Meta requires that whoever generates the Page token can perform the messaging and moderate tasks on the Page, so an editor or viewer role fails here even when everything on the app side looks right. Sort the Page role out before you continue, because the failure it produces later looks like a token problem.
1. Select the Page in the **Page** dropdown. A token appears in the **Page Access Token** field.
2. Click the field to copy the token.
Two behaviors here catch people out. Meta doesn't store the token in this screen, so you can't come back and read it; you generate a fresh one instead. And each time you select the Page from the dropdown, Meta generates a new token, while every token it generated earlier keeps working. That means an accidental extra generation doesn't break anything, but it does leave live credentials you didn't intend to create, so treat each generation as deliberate.
This token is Page-scoped rather than user-scoped. A user token, or a token generated for a different Page, authenticates successfully and then fails to deliver replies.
Artemis routes on the Page's numeric ID, which is a different value from everything else Meta shows you about the Page.
1. Open the Page in Facebook and look under its **About** information, where Facebook lists the Page ID among the page's transparency details.
2. Or open the Page's settings in [Meta Business Suite](https://business.facebook.com/), which also shows it.
Don't substitute the Meta App ID, the Business ID, the Page username from the Page's web address, or the page-scoped user ID. That last one, the PSID, appears in every inbound event and identifies the customer, not the Page: Meta issues each person a different PSID for each Page they message.
The App Secret sits outside the Messenger section, so the left menu is the only route to it.
1. Go to **App settings** -> **Basic**.
2. Click **Show** beside **App secret**.
3. Re-enter your Facebook password when Meta asks, then copy the value.
Artemis uses this secret to verify that an inbound event genuinely came from Meta. Take it from the same app that owns the Messenger product, since an App Secret from a neighboring app produces a signature failure on every single message.
Invent a high-entropy secret string. Meta doesn't issue this one, so you create it and enter the same value twice: once in Artemis, once in the Meta webhook configuration.
Artemis lets you save a connection without it, but Meta's verification can't succeed until a matching non-empty value exists in both places.
While the app stays in development mode, Page tokens reach only Facebook accounts holding an administrator, developer, or tester role on the app.
1. Grant those roles from the **Roles** tab of your app settings, which Meta describes under [standard access](https://developers.facebook.com/docs/messenger-platform/getting-started/quick-start/).
2. Have each person accept the invitation before you test with them.
3. Before ordinary customers can message the Page, submit the app for review on the messaging permissions and complete business verification. Then switch the app to Live mode using the control at the top of the App Dashboard.
A successful test with a role-holding account proves nothing about public access. Plan to repeat it with an account holding no role once the app goes Live.
***
## Artemis channel configuration
Create the connection before you configure the callback in Meta. Meta sends its verification request the moment you save the webhook, and Artemis can only answer that challenge from a stored Verify Token.
1. Open Artemis and select the project.
2. Go to **Deployments** -> **Channels** -> **Messenger** -> **+ New**. The **New Messenger Connection** dialog opens.
3. Configure the following fields:
| Artemis connection field | Source / Notes | What it controls |
| - | - | - |
| Display Name | You choose it | The label operators see in the connections list. Name it for the Page and the environment it serves, because a project can hold several Messenger connections. Routing ignores this value. |
| Facebook Page ID | The Page's **About** information, or Meta Business Suite. See [Meta setup](#meta-setup). | The routing key. It's numeric, and it isn't the App ID, Business ID, Page username, or PSID. |
| Page Access Token | Messenger settings > token generation panel | The credential for outbound replies. Generate it for this exact Page. |
| App Secret | **App settings** -> **Basic** in the same Meta app | The key Artemis uses to verify the signature on every inbound event. |
| Verify Token | You invent it | Answers Meta's one-time verification challenge. Enter the identical value in Meta when you register the callback. |
| Environment | Your published environments | The environment whose active deployment answers incoming messages. Leave the working copy default only while you develop. |
| Provider Verification Strength | Defaults to **Weak** | How far Artemis trusts Meta's identity assertion when it links a person to an existing session. **Weak** suits most deployments. Choose **Strong** only when you trust those assertions enough to base continuity and account-linking decisions on them. |
| Initial Status | Defaults to **Active** | Whether the connection starts handling messages. Set it to **Active**, because Meta's verification challenge resolves only when the connection is already active. |
4. Click **Create**.
5. Open the connection and copy the **Webhook URL** from the **Overview** tab. You need it for the next section.
***
## Webhook configuration in Meta
Registration has two halves, and completing only the first is the most common way a setup fails silently. Meta verifies the URL whether or not you subscribe to anything.
1. Return to the Messenger settings screen in the same Meta app.
2. Find the webhooks area and add a callback for the Page object.
Configure the URL here rather than under another Meta product. A callback registered under a different product verifies successfully and delivers nothing to this connection, which is a slow failure to trace because both screens report success.
1. Paste the Artemis **Webhook URL** exactly as shown.
2. Enter the same **Verify Token** you saved in Artemis.
Don't append a Page ID, a connection ID, a query string, or a trailing segment. Artemis takes the Page ID from the signed payload, so anything added to the path breaks the route rather than refining it.
Save the callback. Meta immediately calls the URL with a subscribe challenge and expects your Verify Token echoed back.
Artemis answers only when the token matches an active connection, so a failure here points at the token, the connection status, or public reachability over HTTPS rather than at the URL itself.
1. In the webhook fields list, click **Manage**.
2. Subscribe the Page webhook to `messages` and `messaging_postbacks`.
The `messages` field carries text, attachments, and quick-reply payloads, so it's required even for an agent that only handles text. Subscribe `messaging_postbacks` as well if your agent sends buttons, since a button tap arrives as a separate event type carrying the authored payload rather than as a message.
1. Confirm this app and webhook list the intended Page as subscribed.
2. Save the app changes.
If Meta asks for permissions review or Page task access for your current app mode, complete that before you test with anyone outside your app roles.
### Validation checklist
| Validation item | Expected result |
| :- | :- |
| Complete Meta setup | Business app created, Messenger product added, and the Page connected with a generated Page Access Token. |
| Collect credentials | Numeric Page ID, Page Access Token, and App Secret copied from the same app and Page. |
| Create the Artemis connection | Connection saved as **Active** with all four values and the intended environment. |
| Register the callback URL | Meta accepts the callback and completes the verification challenge. |
| Subscribe the events | This app and webhook list the Page as subscribed to `messages` and `messaging_postbacks`. |
| Send a live message | A real Messenger conversation produces exactly one agent reply. See [Verify the connection](#verify-the-connection). |
| Retest after Live mode | An account with no app or Page role reaches the agent. |
## Verify the connection
Artemis doesn't synthesize Meta events, so a real Messenger conversation is the only end-to-end proof.
1. While the app sits in development mode, use a Facebook account holding an app or Page role. Open the connected Page in Messenger and send a short message you can search for later. Confirm Meta reports a successful webhook delivery and that Artemis opens one session.
2. Check that a single outbound reply appears in the same conversation. A send failure at this point usually means the token belongs to the wrong Page, the messaging permission is missing, someone revoked the token, or the app mode still restricts delivery.
3. Trigger a quick reply and a button postback, and confirm each returns to the same session. Artemis normalizes both into action events, so the authored action ID and value still have to exist in the agent.
4. Send each message type your Page will use. Artemis downloads image, video, audio, and file attachments before the agent processes them. Location, sticker, and fallback attachments arrive as events rather than as files, so an agent that expects a downloadable attachment won't find one.
5. Once Meta approves the app and you switch it to Live mode, run the same test from an account with no app or Page role. This is the only check that proves ordinary customers can reach the agent.
***
## Messenger limits
Meta enforces these boundaries on outbound content, and Artemis applies them before it calls the Send API. Design your agent's responses to fit them.
| Area | Limit |
| - | - |
| Buttons | Button templates carry up to 3 buttons, with titles capped at 20 characters. |
| Quick replies | Up to 13 choices, with titles capped at 20 characters. |
| Carousels | Generic-template carousels carry up to 10 cards and up to 3 buttons per card. |
| Postback payloads | Up to 1,000 characters, which is the authored value your agent branches on. |
| Messaging window | Meta's standard messaging window lets you reply freely for 24 hours after a person messages the Page. Outside it, you need an approved message tag, which the [Send API reference](https://developers.facebook.com/docs/messenger-platform/reference/send-api/) lists. A valid Page Access Token doesn't bypass this. |
| Media | Remote media has to stay reachable from Artemis and within attachment size, content-type, and network-safety policies. |
***
## Secure the connection
Messenger authentication runs in one direction only, which is easy to misread. Meta signs what it sends you; your Verify Token does nothing after setup.
Every Messenger POST carries `X-Hub-Signature-256`. Artemis computes HMAC-SHA256 over the raw request body using your App Secret and rejects a missing or mismatched signature. Any proxy or middleware in front of Artemis has to preserve the exact request bytes, because reformatting the JSON changes the digest even when the content stays identical.
Keep the three secrets distinct in your own operational notes. The Verify Token protects only the initial challenge and is a string you invented. The App Secret authenticates inbound traffic. The Page Access Token authorizes outbound sends, and you can rotate it without touching the Page ID, so a token rotation never requires a new connection.
Artemis rejects Messenger events older than five minutes as replay protection. Replaying a captured payload during debugging therefore fails by design, and a live message is the only way to retest.
Use separate Meta apps or Pages, credentials, callbacks, and connections for development and production.
***
## Manage the channel connection
Opening a connection shows four tabs. The delete control sits beside the connection name.
**Connection Summary** reports the connection's state and identity.
| Field | Description |
| - | - |
| **Status** | Whether the connection currently handles messages. |
| **Created** and **Updated** | Date and time of creation, and of the last change. |
| **Environment** | The environment whose deployment answers messages. |
| **Source Type** | Reads `Channel Connection`. |
| **Source ID** | Identifies the connection uniquely. Quote it when you raise a support ticket. |
**Setup Instructions** shows the **Webhook URL** to register in Meta and the **Facebook Page ID** this connection matches events to, each with a copy control. Check the Page ID here first when messages arrive and no session appears.
Artemis encrypts credentials after you save them, so each field reads as saved rather than showing its value. Leave a field empty to keep the current value, or enter a new one to replace it.
| Field | Description |
| - | - |
| **Use Auth Profile** | Draws the credentials from a saved auth profile instead of the fields below. Prefer it when the same credentials serve more than one connection, since a rotation then touches one profile. |
| **Page Access Token** | Replace it here when you generate a new token in Meta. The Page ID stays the same, so you don't need a new connection. |
| **App Secret** | Replace it here if you move the Messenger product to a different Meta app. |
| **Verify Token** | Changing this breaks Meta's webhook until you enter the matching value in Meta as well. |
Click **Save Credentials** to apply your changes.
Under **General**, rename the connection with **Display Name**, and revisit **Provider Verification Strength** if your trust posture for this Page changes.
Under **Messenger Settings**, **Facebook Page ID** appears read-only. Artemis routes inbound events by this value, so changing it would orphan the connection. Create a new connection to serve a different Page.
**Authorization Message** controls what the person sees when a tool needs them to authorize something before the conversation continues. Write the template with these variables.
| Scope | Variables | Use |
| - | - | - |
| Message | `{{total}}`, `{{completed}}`, `{{remaining}}` | Summarize progress across every pending authorization, typically in a lead-in line above the list. |
| Repeat block | `{{#authorizations}}` and `{{/authorizations}}` | Wrap the section Artemis repeats once per pending authorization. |
| Item | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Identify and link each authorization inside the repeat block. |
Item variables resolve only inside the repeat block. Use the preview to check the wording, and remember it fills from live pending authorizations at runtime.
Click **Save Changes** to apply your changes.
Choose which deployed version of the agent answers messages on this connection.
Under **Agent Version**, set **Environment** to the environment you want the connection to follow. **Active Deployment** then shows the version currently serving traffic and when someone deployed it.
Check this pair when the channel connects but the agent behaves like an older build. The connection follows whatever the environment currently has deployed, not the version you last edited. Click **Save Changes** after you switch the environment.
***
## Troubleshoot connection issues
| Symptom | Likely cause | Recommended action |
| - | - | - |
| Callback verification fails | The connection doesn't exist yet, the Verify Tokens don't match, or the endpoint isn't publicly reachable | Create the connection before you save the callback, enter the same non-empty Verify Token in both systems, use the URL exactly as Artemis shows it, and confirm the endpoint is reachable over public HTTPS. |
| Inbound requests return 401 | The App Secret came from a different Meta app, or a proxy altered the signed request | Copy the App Secret from the app that sends the webhook, and confirm every proxy preserves the original request body and the `X-Hub-Signature-256` header. |
| Artemis reports no connection found | The Facebook Page ID field holds the wrong identifier | Compare the Page ID in the webhook payload with the numeric value saved in Artemis, and confirm the connection is active. A PSID or an App ID in this field produces exactly this result. |
| Verification succeeded but no messages arrive | The webhook has no field subscription, or the Page isn't subscribed to the app | Subscribe the `messages` and `messaging_postbacks` fields, confirm the app lists the intended Page as subscribed, and check the app mode, tester roles, and outstanding permissions. |
| Messages arrive but replies fail | The token is for the wrong Page, the Page role is insufficient, or no deployment can run | Generate a fresh Page Access Token for the same Page, confirm your account can perform the messaging and moderate tasks on the Page, and confirm the bound environment has a runnable deployment. |
| Everything works for your team and nobody else | The app is still in development mode | Complete app review and business verification, switch to Live, then retest from an account with no role. |
| Text works but buttons fail | The content exceeds a Messenger limit, or the authored action is gone | Keep button and quick-reply counts and label lengths within the limits above, then confirm the authored action still exists in the agent. |
| Text works but attachments fail | The media is unreachable, disallowed, or oversized | Confirm the attachment URL is reachable, that policy allows its host, content type, and size, and that the agent handles the downloaded file. |
| Artemis ignores replayed events | Replay protection rejects events older than five minutes | Expected behavior. Send a new live message instead of replaying a captured payload. |
***
## Meta reference
Use these Meta pages when you need detail beyond what this article covers.
| Resource | Use it for |
| - | - |
| [Messenger Platform quick start](https://developers.facebook.com/docs/messenger-platform/getting-started/quick-start/) | Meta's own walkthrough of app setup, Page token generation, and the first webhook. |
| [Send API reference](https://developers.facebook.com/docs/messenger-platform/reference/send-api/) | Outbound message types, message tags, and the messaging window rules. |
| [My Apps](https://developers.facebook.com/apps/) | Returning to an app you created, or checking which app owns a Page subscription. |
| [Meta Business Suite](https://business.facebook.com/) | Page settings, Page roles, and business verification. |
**Related articles:**
* [Channels](/agent-platform/channels)
* [Set up WhatsApp](/agent-platform/channels/whatsapp)
* [Set up LINE](/agent-platform/channels/line)
* [Set up Telegram](/agent-platform/channels/telegram)
# Set up Microsoft Teams and Copilot
Source: https://koreai.mintlify.app/agent-platform/channels/ms-teams-copilot
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital-channels)
Integrate your agent into Microsoft Teams channels, chats, and Microsoft 365 Copilot through an Azure Bot registration and a Teams application.
## Before you begin
Complete the following tasks in Microsoft before you create the channel connection in Artemis. You need administrator access to the [Azure Portal](https://portal.azure.com/), the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), and the [Teams Admin Center](https://admin.teams.microsoft.com/).
1. Sign in to the [Azure Portal](https://portal.azure.com/) and search for **Azure Bot** in Marketplace.
2. Select **Create** and complete the bot registration.
3. When the deployment completes, select **Go to resource** under **Next steps**.
1. In the Azure Bot resource, go to **Settings** -> **Configuration**.
2. Select the **Manage password** link beside the **Microsoft App ID**. This opens the app registration's **Certificates & secrets** page.
3. Select **+ New client secret** and create the secret.
4. Copy the secret value immediately and store it securely.
5. Go to the app registration's **Overview** page and copy the **Application (client) ID**. This is your Microsoft App ID, also called the Enterprise App ID later in this article.
6. Copy the **Directory (tenant) ID** from the same page.
When you copy the client secret, use the **Value**, not the **Secret ID**. The Secret ID won't authenticate. The Value appears only once. If you navigate away before copying it, create a new client secret.
1. In the app registration, go to **Authentication (Preview)** -> **Supported accounts**.
2. Select **Multiple Entra ID tenants**, choose **Allow all tenants**, and save.
1. Return to the Azure Bot resource and go to **Settings** -> **Channels**.
2. Enable **Microsoft Teams** and **Microsoft 365**.
1. Sign in to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/).
2. Create a new application or import an existing manifest.
3. Go to **Configure** -> **Basic information**, enter the **Short name** and **Long name**, and set the **Application (client) ID** to the Microsoft App ID you copied in step 2.
1. Go to **Configure** -> **App features** and select **Bot**.
2. Enter the Enterprise App ID as the bot ID.
3. Add the project commands, utterances, and descriptions.
4. Select the **Personal**, **Team**, and **Group Chat** scopes.
1. Go to **App package editor** -> **Actions** -> **Edit** to open `manifest.json`.
2. Update the Web App ID (`webApplicationInfo.id`) and the Copilot/Agents ID (`copilotAgents.customEngineAgents.id`) to the Enterprise App ID in both locations.
3. To support file attachments, set `bots[0].supportsFiles` to `true`.
4. Select **Update** and confirm.
Attachment ingestion currently supports personal chat scope only.
1. In the Teams Developer Portal, go to **Publish** -> **Publish to org**, select **Get started**, and publish the application.
2. Open the [Teams Admin Center](https://admin.teams.microsoft.com/) and go to **Teams apps** -> **Manage apps**.
3. Search for the application, open it, and publish it.
## Create the channel connection in Artemis
Open Artemis, select the required project, and go to **Deployments** -> **Channels** -> **MESSAGING** -> **Microsoft Teams** -> **+ New**. On the **New Microsoft Teams Connection** dialog, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection, for example `Microsoft Teams Production`. Up to 255 characters. |
| **Bot App ID** | The Microsoft App ID of the bot configured in your Teams application. Up to 255 characters. |
| **App ID** | The Microsoft App ID from the Azure Bot registration, also called the Enterprise App ID. |
| **Client Secret** | The client secret value you created in the Azure app registration. |
| **Azure Tenant ID** | The Directory (tenant) ID from the app registration's Overview page in the Azure Portal. |
| **Environment** | Select an environment to follow its active deployment. If no environment has an active deployment yet, leave the default to use the working copy and bind the connection later from the **Deployment** tab. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. Set this to **Active**. |
**Bot App ID** and **App ID** both take a Microsoft App ID. The app ID of your Teams app must match the app ID in the Azure Bot registration and the values you enter here. A mismatch is the most common cause of a Teams integration that won't connect.
Select **Create**.
1. Open the connection you created and copy the **Webhook URL** from the **Overview** tab.
2. In the [Azure Portal](https://portal.azure.com/), open the Azure Bot resource and go to **Configuration**.
3. Paste the Webhook URL into the **Messaging endpoint** field.
4. Enable the **Streaming Endpoint** and select **Apply**.
1. Open [Teams](https://teams.cloud.microsoft/) and go to **Apps**.
2. Search for the published application, open it, and select **Add**. To test in Copilot, select **Open with Copilot**.
3. Trigger the configured queries and verify the Artemis response.
## Key values reference
| Value | Where it comes from | Where it's used |
| - | - | - |
| **Microsoft App ID (Enterprise App ID)** | The Azure Bot registration's Application (client) ID | The **Bot App ID** and **App ID** fields in Artemis, the bot ID in the Teams application, and the app package IDs in the Teams Developer Portal |
| **Client Secret** | Created under **Certificates & secrets**; copy the value immediately after creation | The **Client Secret** field in Artemis |
| **Azure Tenant ID** | The Directory (tenant) ID in the Azure Portal (admin account) | The **Azure Tenant ID** field in Artemis |
| **Webhook URL** | The Artemis Microsoft Teams connection's **Overview** tab | The Azure Bot **Messaging endpoint** |
## Manage the channel connection
Opening a channel from **Deployments** -> **Channels** -> **MESSAGING** -> **Microsoft Teams** shows four tabs: **Overview**, **Credentials**, **Configuration**, and **Deployment**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | For example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | For example: `staging` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique channel identifier (UUID) |
Under **Setup Instructions**, the tab shows the **Webhook URL** to set as the bot's messaging endpoint in Azure, the **Bot App ID** for reference, and the **Require verified Teams identity** toggle.
#### Require verified Teams identity
Turn this on to require Microsoft-resolved Teams profile identity before the connection starts or resumes a session. Leave it off for baseline Teams conversations.
When the toggle is on, Runtime resolves the sender's Microsoft profile email or UPN through the Bot Connector member endpoint before it allows the turn. If the identity can't be resolved, the turn doesn't proceed. This confirms that the person on the other end is the Teams user their profile claims they are, which matters for agents that act on a user's behalf or return information scoped to that user.
The toggle relies on the credentials already configured for this connection or its selected auth profile, and on the bot staying installed and reachable in Teams. It doesn't use Microsoft Graph consent, OBO settings, or Graph scopes.
The **Stronger identity verification setup** panel below the toggle lists the specific requirements to complete when you enable it.
Credentials are saved and encrypted after creation; the fields show placeholder text and accept new values only if you want to update them.
| Field | Description |
| - | - |
| **Use Auth Profile** | Toggle on to authenticate through a saved Auth Profile instead of entering credentials manually. When enabled, select the profile from the dropdown in place of the fields below. |
| **App ID** | Your Microsoft App ID. Shown as *(saved — enter to update)*; leave blank to keep the current value. |
| **Client Secret** | The client secret from your Azure Bot registration. Shown as *(saved — enter to update)*; leave blank to keep the current value. |
| **Azure Tenant ID** | The Directory (tenant) ID from the Azure Overview page. Shown as *(saved — enter to update)*; leave blank to keep the current value. |
Select **Save Credentials** to apply any changes.
Under **General**, update the **Display Name**.
Under **Microsoft Teams Settings**, configure the following.
| Field | Description |
| - | - |
| **Bot App ID** | The Microsoft App ID of the bot, shown for reference. You can't change it after creation. |
| **Enable Streaming** | Under **Response Streaming**, turn this on to stream LLM responses to Teams as the agent generates them, instead of waiting for the full response. Turn it off to deliver each response as a single message. |
Select **Save Changes** to apply any changes.
The **Deployment** tab shows these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Staging`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and when it was deployed. |
Select **Save Changes** after switching the environment or version.
# Set up Slack
Source: https://koreai.mintlify.app/agent-platform/channels/slack
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital-channels)
Deploy your agent as a Slack bot for team collaboration. Connect your Slack app to Artemis by entering its credentials manually.
**Redirect URL and Request URL are different.** Slack uses two URLs at different stages:
* **OAuth Redirect URL** — used only during the **Add to Slack** authorization flow, registered under **OAuth & Permissions** so Slack can return authorization back to Studio.
* **Events Request URL** — generated for this connection and pasted under **Event Subscriptions → Request URL** in Slack, allowing Slack to deliver messages and events to Studio.
## Create an app in Slack
At [api.slack.com/apps](https://api.slack.com/apps), select **Create New App**, choose **From scratch**, name the app, and choose a development workspace.
You need three values from Slack before you can create the connection in Artemis:
| Value | Where to find it |
| - | - |
| **Team ID** | Open your workspace in a browser. In `https://app.slack.com/client/T.../...` the segment beginning with `T` is your Team ID. |
| **App ID** | Go to **Basic Information** -> **App Credentials** and copy **App ID**. |
| **Signing Secret** | On the same **App Credentials** panel, select **Show** next to **Signing Secret** and copy it. |
Go to **OAuth & Permissions** and, under **Bot Token Scopes**, add:
| Scope | Purpose |
| - | - |
| `chat:write` | Send messages as the bot. |
| `im:history` | Read direct message history with the bot. |
| `commands` | Required for slash commands. |
| `app_mentions:read` (optional) | Read @mentions of the bot in channels. |
| `files:read` (optional) | Required for file attachments. |
| `chat:write.public` (optional) | Required to post to public channels the bot hasn't joined. |
On the same **OAuth & Permissions** page, select **Install to Workspace** (or **Reinstall to Workspace** if the app is already installed). Once installed, copy the **Bot User OAuth Token** from **OAuth Tokens** — it begins with `xoxb-`.
Reinstall the app whenever you add or change scopes so the bot token receives the new grants.
## Integrate the Slack app in Artemis
Open Artemis, select the required project, and go to **Deployments** -> **Channels** -> **MESSAGING** -> **Slack** -> **+ New**.
Select **Enter credentials manually instead**, then fill in the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection, for example `Slack Production`. Up to 255 characters. |
| **Slack Team ID:App ID** | The Team ID and App ID from Slack, joined by one colon with no spaces, for example `T0xxxxxxxxx:A0xxxxxxxxx`. |
| **Bot Token** | The Bot User OAuth Token from **OAuth & Permissions** -> **OAuth Tokens**. Must begin with `xoxb-`. |
| **Signing Secret** | The signing secret from **Basic Information** -> **App Credentials**. |
| **Environment** | Select an environment to follow its active deployment. |
| **Provider Verification Strength** | **Weak (default)** is set by default. Choose **Strong** only for channels/providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections can be used immediately. **Inactive** connections stay saved but disabled until you activate them. |
Select **Create**.
Open the connection you created and copy the **Webhook URL** from the **Overview** tab. Go back to your Slack app's **Event Subscriptions** page, turn **Enable Events** on, and paste the Webhook URL into **Request URL**. Wait for Slack to show **Verified**, then, under **Subscribe to bot events**, add:
| Event | Description |
| - | - |
| `message.im` | Direct messages sent to the bot. |
| `app_mention` | @mentions in channels where the app is invited. |
Save your changes.
Send the app a direct message, then invite it to a channel and @mention it. Confirm the agent replies in both places.
## Manage the channel connection
Opening a channel from **Deployments** -> **Channels** -> **MESSAGING** -> **Slack** shows four tabs: **Overview**, **Credentials**, **Configuration**, and **Deployment**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | For example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | For example: `staging` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique channel identifier (UUID) |
Under **Setup Instructions**:
* **Require verified Slack identity** - Toggle on to require Slack workspace user identity with a resolvable email before this connection starts or resumes a session. Guests, external Slack Connect users, and users without email remain blocked.
Before enabling this policy, complete the **Verified Slack identity setup**. Runtime uses Slack's `users.info` to resolve the sender's workspace email.
1. In the Slack app's **OAuth & Permissions**, add these Bot Token Scopes: `users:read` and `users:read.email`.
2. Reinstall or re-authorize the Slack app after adding scopes so the bot token receives the new permissions.
* The tab also shows the **Webhook URL** to paste into Slack's Event Subscriptions **Request URL**, and the **Slack Team ID:App ID** for reference. Select the copy icon next to either value to copy it.
Credentials are saved and encrypted after creation; the fields show placeholder text and accept new values only if you want to update them.
| Field | Description |
| - | - |
| **Use Auth Profile** | Toggle on to authenticate through a saved Auth Profile instead of entering credentials manually. When enabled, select the profile from the dropdown in place of the fields below. |
| **Bot Token** | The Bot User OAuth Token from Slack's **OAuth & Permissions** -> **OAuth Tokens**. Shown as *(saved — enter to update)*; leave blank to keep the current value. |
| **Signing Secret** | The signing secret from Slack's **Basic Information** -> **App Credentials**. Shown as *(saved — enter to update)*; leave blank to keep the current value. |
Select **Save Credentials** to apply any changes.
Under **General**, update the **Display Name** and **Provider Verification Strength**.
Under **Slack Settings**, the following fields apply.
| Field | Description |
| - | - |
| **Slack Team ID:App ID** | The Team ID and App ID entered at creation, shown for reference. You can't change it after creation. |
Under **Response Streaming**, configure the following.
| Field | Description |
| - | - |
| **Enable Streaming** | Stream LLM responses to Slack in real time instead of waiting for the full response. |
| **Chunk Size (characters)** | Minimum characters to buffer before sending to Slack. Lower values feel faster but make more API calls. |
Under **Authorization Message**, configure the message shown when a tool needs the user to authorize.
| Field | Description |
| - | - |
| **Message template** | The message text, with insertable variables: `{{#authorizations}}`, `{{profileName}}`, `{{authLink}}`, and `{{remaining}}`. Select **See all variables** for the full list, or **Show preview** to preview the rendered message. |
Select **Save Changes** to apply any changes.
The **Deployment** tab shows these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Staging`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and when it was deployed. |
Select **Save Changes** after switching the environment or version.
## Optional Slack features
| Feature | Setup |
| - | - |
| **Buttons, menus, and shortcuts** | Enable **Interactivity & Shortcuts** and use the same connection-specific Events Request URL as the **Interactivity Request URL**. |
# Set up Telegram
Source: https://koreai.mintlify.app/agent-platform/channels/telegram
[Back to messaging channels](/agent-platform/channels#messaging-or-digital-channels)
Connect a Telegram bot to your agent so users reach it in private chats, groups, and supergroups. Artemis registers the Telegram webhook for you and protects it with a generated secret, so you never call `setWebhook` yourself.
You create the bot in Telegram, then hand Artemis two values from it. Everything after that happens automatically when you create the connection.
## How Artemis routes Telegram messages
Four pieces carry the integration, and each one does a different job. Confusing them causes most setup failures.
| Piece | Who owns it | What it does |
| - | - | - |
| **Bot Username** | BotFather | Routes incoming updates. Artemis puts the normalized username in the webhook path, then matches the last path segment to this connection. |
| **Bot Token** | BotFather | Authorizes every outbound call Artemis makes to the Telegram Bot API, including `setWebhook`, `sendMessage`, typing indicators, callback answers, and media downloads. |
| Secret token | Artemis | Verifies that an incoming update really came from Telegram. Artemis generates it, stores it encrypted, and never asks you for it. |
| Webhook URL | Artemis | Receives updates from Telegram. Artemis builds it from your bot username and registers it during creation. |
Because the username sits in the URL path, you can't change it after creation. Create a new connection if you need to point at a different bot.
## Create the Telegram bot
Do this work in Telegram before you open Artemis. BotFather is a bot itself, so you create your bot by holding a chat with it rather than by filling in a web form. See Telegram's [introduction for developers](https://core.telegram.org/bots) if you've never built a bot before.
Search Telegram for `@BotFather`, or open [t.me/botfather](https://t.me/botfather) from any device signed in to your account. Several accounts imitate BotFather, so confirm the blue verification badge beside the name before you send anything. A fake BotFather exists to harvest tokens.
Select **Start** if you've never used it. BotFather replies with its command list.
Send `/newbot`. BotFather asks two questions in turn.
First it asks for a name, which is the title users see at the top of the chat. Spaces and most characters work here, and you can change it later.
Then it asks for a username, which is permanent and must be unique across Telegram. Usernames run 5 to 32 characters, accept Latin letters, numbers, and underscores, ignore case, and must end in `bot`. BotFather rejects a taken username and asks again, so have a second choice ready.
Choose carefully. Telegram never lets you change a bot username, and Artemis builds the webhook path from it, so a rename means a new bot and a new connection in both systems.
BotFather confirms with a message containing the bot link and the token. The token is the long string after the numeric bot ID and a colon.
Copy the username from the confirmation message without the `t.me/` prefix that surrounds it in the link. Artemis strips a leading `@` and stores the normalized value, so either form works. Nothing else routes: a display name, a numeric bot user ID, a chat ID, a token fragment, or a full URL all fail.
Copy the token separately. Telegram formats it as a tappable code block, so tapping or clicking it copies the whole value.
To retrieve either value later, send `/mybots`, select the bot, and select **API Token**. BotFather shows the username and token again without creating anything new.
Return to BotFather and use these commands to control what users see before and during a chat. Each one prompts you to pick the bot first.
| Command | What it sets |
| - | - |
| `/setname` | The title users see at the top of the chat. This one you can change whenever you like. |
| `/setdescription` | The text on the empty chat screen, shown before a user selects **Start**, up to 512 characters. |
| `/setabouttext` | The short line on the bot's profile page, which travels with the link when someone shares the bot, up to 120 characters. |
| `/setuserpic` | The bot's profile photo. |
| `/setcommands` | The command menu Telegram suggests as a user types `/`. |
These shape discovery only. They don't affect message delivery, and they don't replace the webhook Artemis registers. Changes can take a few minutes to appear. Telegram documents the full command list under [BotFather](https://core.telegram.org/bots/features#botfather), and offers the same settings through the BotFather Mini App if you prefer a visual interface to chat commands.
Artemis processes a group or supergroup message only when the message mentions the bot, replies to one of its messages, or uses `/start` or `/help`. Ordinary group chatter never reaches your agent.
This holds whatever you set in BotFather. Telegram's own [privacy mode](https://core.telegram.org/bots/features#privacy-mode), which you toggle with `/setprivacy`, widens what Telegram sends when you disable it, and a bot added to a group as an admin receives everything regardless. Artemis still applies the mention and reply rule on top, so neither change alters what your agent sees, and the bot stays quiet in busy groups by design.
One Telegram behavior does matter here: a privacy mode change takes effect only after you remove the bot from the group and add it back.
Create one bot for development and another for production, each with its own token and its own Artemis connection. Telegram allows a single webhook per bot, so sharing one bot across environments means the two registrations overwrite each other.
The bot token authorizes every Bot API call for that bot, and it travels in the request URL. Treat it like a password: keep it out of tickets, screenshots, logs, browser code, and client-side configuration. If it leaks, send `/token` to BotFather to generate a replacement, then update the credential in Artemis. The old token stops working as soon as the new one issues.
## Connect the bot in Artemis
The connection stores your bot identity, binds the agent version that answers, and registers the webhook. Go to **Deployments** -> **Channels** -> **Telegram** -> **+ New** and complete the following.
| Field | What it controls |
| - | - |
| **Display Name** | The label operators see in the connections list. Name it for the bot and the environment it serves, because a project can hold several Telegram connections. Routing ignores this value. |
| **Bot Username** | The routing key. Enter the BotFather username exactly, with or without a leading `@`. Artemis writes the normalized value into the webhook path and can't change it later. |
| **Bot Token** | The credential for every outbound Bot API call. Paste the complete token, including the numeric bot ID and the colon that precedes the secret portion. Artemis encrypts it on save. |
| **Environment** | The published environment whose active deployment answers incoming messages. Selecting one keeps the connection current as you promote new versions. Leave the working copy default only while you develop. |
| **Provider Verification Strength** | How far Artemis trusts Telegram's identity assertion when it links a user to an existing session. **Weak** suits most deployments. Choose **Strong** only when you trust the provider's assertions enough to base continuity and account-linking decisions on them. |
| **Initial Status** | Whether the connection starts handling messages. **Active** takes traffic immediately. **Inactive** saves the configuration in a disabled state, which suits a connection you're staging ahead of a launch. |
### What happens when you select Create
Artemis completes the Telegram-side registration in one step, so you don't touch the Bot API yourself.
Artemis generates a high-entropy secret token, calls `setWebhook` with the URL built from your bot username, and subscribes to `message` and `callback_query` updates only. It also drops any updates left pending from an earlier webhook on that bot, so stale traffic can't enter the new connection. Messages users sent before you created the connection won't appear, by design.
If registration fails, Artemis rolls the whole creation back rather than leaving a connection that can't receive anything. A conflict at this point means the token already belongs to another active Telegram connection in the project. Telegram permits one webhook per bot, so Artemis refuses the second connection instead of silently replacing the first one's webhook.
## Verify the connection
Run these checks before you hand the bot to users. Each one exercises a different path, and a bot that answers private messages can still fail in groups or on buttons.
Open the bot by its exact username, select **Start**, and send a short message you can search for later. Confirm you get one reply and that Artemis opens a single session.
Send `/start`, then open a deep link carrying a start payload if your agent uses one. Artemis treats the command as a welcome action and passes the payload through as its value, so your agent can branch on where the user came from. Telegram documents the link format under [deep linking](https://core.telegram.org/bots/features#deep-linking).
Add the bot to a group or supergroup, then address it by username, reply to one of its messages, and send `/start` or `/help`. Each should reach the agent. Send an unrelated message too, and confirm the bot ignores it.
Trigger a response that carries an inline keyboard and select a button. Confirm the loading spinner clears and the resulting action lands in the same chat session.
Send a photo, document, audio file, video, and voice note, depending on what your agent accepts. Artemis downloads the largest photo size or the attached file before the agent processes it. Include a caption in at least one test, since captions travel separately from the file.
## Manage the connection
Opening a connection shows four tabs. The delete control sits beside the connection name.
**Connection Summary** reports the connection's state and identity.
| Field | Description |
| - | - |
| **Status** | Whether the connection currently handles messages. |
| **Created** | Date and time of creation. |
| **Updated** | Date and time of the last change. |
| **Environment** | The environment whose deployment answers messages. |
| **Source Type** | `Channel Connection`. |
| **Source ID** | The connection's internal identifier, useful when you raise a support request. |
**Setup Instructions** shows the **Webhook URL** Artemis registered with Telegram and the normalized **Bot Username**, each with a copy control. The URL follows the form `https://{host}/api/v1/channels/telegram/webhook/{bot-username}` and ends with the username minus any leading `@`.
Use the **Webhook URL** to confirm what Telegram should be calling. If `getWebhookInfo` reports a different URL, something outside Artemis re-registered the bot.
Update the bot token here when BotFather issues a new one.
Artemis encrypts the stored token and never displays it again, so the field stays empty until you enter a replacement. Entering a new value and selecting **Save Credentials** overwrites the old one.
Turn on **Use Auth Profile** to draw the credential from a stored authentication profile instead of holding a token on this connection.
Rotate the credential whenever BotFather revokes or regenerates the token. Until you do, Artemis can neither re-register the webhook nor send messages, because both calls carry the old token.
Adjust how the connection presents itself and how it handles responses.
Under **General**, rename the connection with **Display Name**, and revisit **Provider Verification Strength** if your trust posture for this bot changes.
Under **Telegram Settings**, **Bot Username** appears read-only. Artemis built the webhook path from it at creation, so changing it would break routing. Create a new connection to target a different bot.
Turn on **Enable Streaming** to send the agent's response to Telegram as the model produces it, rather than holding the message until the response completes. Streaming shortens perceived latency on long answers.
**Authorization Message** controls what the user sees when a tool needs them to authorize something before the conversation continues. Write the template with these variables.
| Scope | Variables | Use |
| - | - | - |
| Message | `{{total}}`, `{{completed}}`, `{{remaining}}` | Summarize progress across every pending authorization, for example in a lead-in line before the list. |
| Repeat block | `{{#authorizations}}` and `{{/authorizations}}` | Wrap the section Artemis repeats once per pending authorization. |
| Item | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Identify and link each authorization inside the repeat block. |
Item variables resolve only inside the repeat block. Use the preview to check the wording, and remember it fills from live pending authorizations at runtime.
Choose which deployed version of the agent answers messages on this connection.
Under **Agent Version**, set **Environment** to the environment you want the connection to follow. **Active Deployment** then shows the version currently serving traffic. Select **Save Changes** to apply the binding.
A connection left on the working copy shows a warning, because the working copy is a development target that changes as you edit the agent. Bind the connection to a published environment before you put the bot in front of users.
## Telegram limits
Telegram enforces these boundaries, and Artemis applies them before it calls the Bot API. Design your agent's responses to fit them.
| Area | Limit |
| - | - |
| Text | Outbound messages run 1 to 4096 characters. Artemis substitutes a single ellipsis when a response would otherwise be empty. |
| Inline keyboards | Up to 10 rows, with button text capped at 64 characters. |
| Callback data | 64 bytes. Keep authored action IDs and values short enough to survive encoding. |
| Media | Files up to 20 MB download, which is the Bot API's own ceiling rather than an Artemis one. Artemis rejects anything larger before upload. |
| Channel posts | Not supported. The connection serves private chats, groups, and supergroups under the mention and reply rule. |
## Operate the connection safely
A Telegram connection depends on two secrets and one webhook registration. Protect all three.
The secret token is separate from the bot token. Telegram returns it in the `X-Telegram-Bot-Api-Secret-Token` header on every update, and Artemis compares it with the encrypted stored value using a constant-time check before processing anything. A proxy that strips unrecognized headers breaks this check, so preserve the header end to end.
Never call `setWebhook` yourself for a connected bot. Telegram keeps one webhook per bot, so registering another URL or deleting the webhook outside Artemis stops the connection until you re-save or recreate it. Use `getWebhookInfo` for diagnosis only, run it through a secure operator tool, and keep the real token out of any transcript you share.
Artemis deduplicates on Telegram's `update_id`, so a Telegram retry doesn't produce a duplicate agent turn.
## Troubleshooting
Match the symptom to the check. Most failures trace to the token, the webhook registration, or the group mention rule.
| Symptom | What to check |
| - | - |
| Creation fails with a webhook registration error | Verify the bot token, confirm the Artemis host is reachable over public HTTPS, confirm outbound access to `api.telegram.org`, and confirm BotFather permits `setWebhook` for the bot. |
| Creation returns a conflict | The token already belongs to another active Telegram connection in this project. Reuse that connection or deactivate it rather than replacing its webhook. |
| Updates return 401 | An external `setWebhook` call may have replaced the secret, or a proxy dropped the `X-Telegram-Bot-Api-Secret-Token` header. Re-save or recreate the connection and preserve the header. |
| Private messages don't arrive | Confirm the user selected **Start**, check that `getWebhookInfo` points at this connection's URL, and confirm the connection is active. |
| Private chats work but groups don't | Mention the exact bot username or reply to one of its messages, test `/start` and `/help`, and confirm the bot is a member of the group. Disabling privacy mode in BotFather doesn't make Artemis process unmentioned chatter. |
| Text works but buttons don't | Confirm the callback payload fits within 64 bytes and that the authored action still exists. |
| Text works but files fail | Confirm your agent accepts the media type, that the file is 20 MB or smaller, and that your attachment policy allows it. A failure here often means the token changed, since Artemis downloads through the Bot API. |
| Messages sent before creation never arrive | Expected. The initial registration drops pending updates so stale events can't enter the new connection. |
# Set up Twilio SMS
Source: https://koreai.mintlify.app/agent-platform/channels/twilio
[Back to messaging or digital channels](/agent-platform/channels#messaging-or-digital-channels)
Connect a Twilio phone number to your agent so customers reach it over SMS and MMS. You prepare a messaging-capable sender in Twilio, hand Artemis the account credentials, and then point the number's incoming-message webhook at a connection-specific URL that Twilio signs on every request.
The setup requires:
1. [Twilio setup](#twilio-setup): Choose the project, get a messaging-capable number, complete sender compliance, and copy the Account SID and Auth Token.
2. [Artemis channel configuration](#artemis-channel-configuration): Configure the connection using those credentials, then copy the generated webhook URL.
3. [Webhook configuration in Twilio](#webhook-configuration-in-twilio): Set the number's incoming-message webhook to that URL and select HTTP POST.
After you complete the configuration, an SMS to your Twilio number opens a session in Artemis, the bound deployment answers it, and the reply goes back to the sender through Twilio's Messages API.
**Important considerations**
* **The reply doesn't travel in the webhook response**: Artemis returns an empty TwiML acknowledgement immediately and queues the agent, then sends the reply as a separate outbound API call. A 2xx on the webhook proves delivery of the inbound message and nothing more.
* **Twilio signs the webhook URL itself**: The signature covers the full external URL and the sorted form fields. Scheme, host, port, and path all participate, so a proxy that rewrites any of them breaks signature validation even though the request arrives intact.
* **One account boundary**: The Account SID, Auth Token, phone number, and any Messaging Service SID have to come from the same Twilio project or subaccount. Mixing a subaccount number with parent-account credentials fails in ways that look like a configuration error.
* **Compliance sits outside Artemis**: Sender registration, consent, and opt-out rules belong to Twilio and the carriers. A working webhook doesn't exempt your traffic from them.
## Prerequisites
* A Twilio account with access to the project or subaccount that owns the sender and its billing.
* A messaging-capable Twilio phone number for the destination countries, in E.164 format with the leading plus and country code.
* Any sender registration your traffic needs, such as [A2P 10DLC](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc) brand and campaign registration for United States long-code traffic.
* Artemis project access with permission to open **Deployments** -> **Channels**, and a deployed agent in the environment you intend to bind.
***
## How Artemis routes Twilio SMS messages
Twilio calls a URL you choose per phone number, so Artemis puts the connection identifier in the path rather than reading it from the payload.
| Piece | Who owns it | What it does |
| - | - | - |
| **Connection Identifier** | You | Routes inbound messages. Artemis appends it to the webhook path and resolves the connection from that segment, so the identifier has to survive in the URL exactly as Twilio calls it. |
| **Account SID** | Twilio | Identifies the account for outbound calls to the Messages API and for authenticated MMS downloads. It starts with `AC`. |
| **Auth Token** | Twilio | Does two jobs. Artemis uses it to validate the `X-Twilio-Signature` header on every inbound request, and to authenticate outbound API calls. |
| **Messaging Service SID** | Twilio | Optional. When present, Artemis sends replies through that service instead of replying from the inbound number. It starts with `MG`. |
| **Webhook URL** | Artemis | Receives the incoming-message callback. It ends with the connection identifier, and its exact external form is part of the signature. |
Artemis keys each conversation to the Twilio number and the customer number together, so the same person messaging two of your Twilio numbers holds two separate sessions.
***
## Twilio setup
Do this work in the [Twilio Console](https://www.twilio.com/console) before you open Artemis.
Sign in to the Twilio Console, then use the account selector at the top of the page to switch to the project or subaccount that owns the production phone number and its billing.
Everything you collect from here has to come from this one account boundary. Twilio scopes the Auth Token to the account, so credentials from a parent account can't authenticate a subaccount's traffic.
1. Go to **Phone Numbers** -> **Manage** -> **Active numbers**.
2. Confirm the number you intend to use lists SMS capability, and MMS as well if your agent will handle media.
3. If you need a new number, buy one from **Phone Numbers** -> **Buy a number**, filtering by the capabilities and country you need.
Keep the full E.164 value, including the leading plus and country code. Capability varies by country, so a number that sends SMS in one market may not support MMS in another.
Finish the registration, sender-verification, consent, and opt-out requirements Twilio shows for your countries and traffic type.
United States long-code traffic needs A2P 10DLC brand and campaign registration. Carriers filter unregistered traffic rather than failing it loudly, so messages appear to send and never arrive. Twilio surfaces this as error 30034.
Configuring a webhook doesn't bypass any of this. Start compliance early, because it waits on external review rather than on you.
1. Open the Twilio Console dashboard for the selected account.
2. Find the account information panel, which shows the **Account SID** and the **Auth Token**.
3. Copy the `AC`-prefixed Account SID, then reveal and copy the Auth Token.
The Auth Token authenticates outbound API calls and validates inbound request signatures, so treat it as a high-value secret and store it in your secret manager rather than in a ticket or a shared document.
A Messaging Service is optional. Use one when you want Twilio to pick a sender from a pool, apply sticky sender behavior, or carry an A2P campaign registration.
1. Go to **Messaging** -> **Services** and create a service, or open an existing one.
2. Add your phone number to the service's sender pool. Twilio's [sender pool guidance](https://help.twilio.com/articles/4402705042075-Managing-a-Messaging-Service-Sender-Pool) covers the rules.
3. Copy the `MG`-prefixed SID.
Leave the SID out of the Artemis connection to reply from the inbound number instead. Twilio's [Messaging Services documentation](https://www.twilio.com/docs/messaging/services) explains what each mode changes.
A trial project restricts which recipients and senders you can use, and adds branding to outbound messages.
Prove production behavior on an upgraded account with an approved sender. A trial test that works tells you the wiring is right, not that your launch will work.
***
## Artemis channel configuration
Create the connection before you configure the Twilio webhook. The callback URL contains the connection identifier, so you can't finalize it in Twilio until Artemis has generated it.
1. Open Artemis and select the project.
2. Go to **Deployments** -> **Channels** -> **Twilio SMS** -> **+ New**.
3. Configure the following fields:
| Artemis connection field | Source / Notes |
| - | - |
| Display Name | The label operators see in the connections list. Name it for the sender and the environment it serves, because a project can hold several Twilio connections. Routing ignores this value. |
| Connection Identifier | The routing key, which Artemis writes into the webhook path. Choose a short, stable, URL-safe value that identifies this sender. You can't change it after creation, because the webhook URL is built from it. |
| Account SID | Identifies the Twilio account for outbound calls and MMS downloads. From the account information panel in the Twilio Console dashboard. |
| Auth Token | Validates the signature on inbound requests and authenticates outbound API calls. From the same account as the Account SID. |
| Messaging Service SID | Optional. Supply it to send replies through that service. Leave it empty to reply from the number that received the message. |
| Environment | The published environment whose active deployment answers incoming messages. Leave the working copy default only while you develop. |
| Provider Verification Strength | How far Artemis trusts Twilio's identity assertion when it links a sender to an existing session. **Weak** suits most deployments. Choose **Strong** only when you trust those assertions enough to base continuity and account-linking decisions on them. |
| Initial Status | Whether the connection starts handling messages. **Active** takes traffic immediately. **Inactive** saves the configuration in a disabled state, which suits a connection you're staging ahead of a launch. |
4. Select **Create**.
5. Open the connection and copy the **Webhook URL** from the **Overview** tab.
The URL ends with your connection identifier. Copy it exactly, because Twilio signs the complete URL and any difference invalidates the signature.
### Validation checklist
| Validation item | Expected result |
| :- | :- |
| Select the Twilio account | Account SID, Auth Token, phone number, and any Messaging Service all come from one project or subaccount. |
| Confirm sender capability | Number lists SMS capability for the destination countries, and MMS if the agent handles media. |
| Complete compliance | Sender registration, consent, and opt-out requirements finished for the traffic type and region. |
| Create the Artemis connection | Connection saved as Active with a stable connection identifier and the account credentials. |
| Copy the webhook URL | Webhook URL copied from the **Overview** tab, ending with the connection identifier. |
| Configure the Twilio webhook | The number's incoming-message webhook points at that exact URL with HTTP POST selected. |
| Bind a deployment | The connection's **Deployment** tab shows a runnable active deployment. |
| Send a real SMS | Test message reaches the agent and the reply arrives once. See [Verify the connection](#verify-the-connection). |
***
## Webhook configuration in Twilio
Twilio calls this URL every time the number receives a message. Configure it on the number itself, or on the Messaging Service when that service owns inbound handling.
For a direct number, go to **Phone Numbers** -> **Manage** -> **Active numbers** and select the number.
For a Messaging Service that owns inbound handling, open the service and use its integration settings instead. Configure one or the other, because two handlers pointing at the same connection produce duplicate replies.
1. Scroll to the **Messaging** section of the number's configuration.
2. In the **A message comes in** field, select **Webhook**.
3. Paste the Artemis **Webhook URL** exactly as shown, including the connection identifier at the end.
4. Select **HTTP POST** from the method dropdown.
5. Select **Save**.
Twilio sends `application/x-www-form-urlencoded` POST fields including `MessageSid`, `AccountSid`, `From`, `To`, `Body`, `NumMedia`, and any `MediaUrl` fields. Twilio's [incoming message webhook reference](https://www.twilio.com/docs/usage/webhooks/sms-webhooks) lists them all.
Twilio signs the complete URL plus the sorted form fields using HMAC-SHA1 with your Auth Token, then sends the result in `X-Twilio-Signature`. Artemis validates the signature using its configured public base URL.
Configure that base URL to the same external address Twilio calls, and forward the `X-Twilio-Signature` header unchanged. HTTPS termination and reverse-proxy host and protocol settings all have to agree, because scheme, host, port, path, and form values all participate in validation.
Artemis returns HTTP 200 with an empty TwiML response straight away and queues the agent. Twilio treats that as an acknowledgement and sends nothing to the customer.
The agent's reply follows separately through the Messages API. Twilio expects a response within 15 seconds and calls the fallback webhook when the primary fails or times out, which is why the acknowledgement returns before the agent runs.
***
## Verify the connection
Artemis doesn't synthesize Twilio requests, so a real SMS is the only end-to-end proof.
1. From an allowed recipient, send a short SMS to the Twilio number that you can search for later.
On a trial project, the recipient has to be a verified caller ID.
2. Confirm Twilio reports a 2xx webhook result and that Artemis opens one session.
3. Check that the agent replies once to the original sender.
The reply arrives from the inbound Twilio number, or from the Messaging Service when you supplied its SID. The empty TwiML acknowledgement isn't the reply, so a 2xx with no message means the outbound path failed.
4. Send a second message from the same customer number to the same Twilio number, and confirm it continues the existing session rather than starting a new one.
A different sender or recipient number opens a separate conversation.
5. Send an MMS where the number, destination, and carrier support it, and confirm the agent handles the downloaded attachment.
6. Repeat the whole test on an upgraded account with the production sender and an ordinary recipient, then check opt-in and opt-out handling, delivery status, throughput, and per-segment cost.
***
## SMS behavior and limits
SMS is a plain-text channel with carrier economics attached, which changes how you author agent responses.
| Area | Behavior |
| - | - |
| Formatting | SMS carries plain text only. Markdown, buttons, and rich cards have no equivalent, so author replies that read correctly without them. |
| Message length | Carriers split long messages into multiple segments, and each segment bills separately. Unicode characters reduce the per-segment character count, so test representative content for your target countries. |
| Delivery retries | Twilio delivers webhooks at least once and makes no ordering guarantee, so the same inbound message can arrive more than once. |
| Media | MMS availability depends on the number, the destination, and the carrier. Artemis authenticates downloads from allowed Twilio hosts using the Account SID and Auth Token, then applies attachment type and size policy. |
| Timeout | Twilio expects a webhook response within 15 seconds and falls back to the secondary handler after that. |
| Compliance | Consent, opt-out keywords, and sender registration apply to every message regardless of how the agent behaves. |
***
## Secure the connection
Twilio authentication rests on one credential doing two jobs, and on a URL that both sides have to match exactly.
Every inbound request carries `X-Twilio-Signature`. Artemis computes HMAC-SHA1 over the configured public callback URL and the sorted form parameters using the Auth Token as the key. Twilio's [security guide](https://www.twilio.com/docs/usage/security) describes the same calculation from the sending side.
Treat the external URL as signed data. Keep HTTPS termination, host, protocol, and port aligned with the public base URL you configured for Artemis, and pass the signature header through untouched. A reverse proxy that presents a different internal URL to Artemis breaks validation even though nothing about the message changed.
The Auth Token validates inbound signatures and authorizes outbound sends, so rotate it in Twilio and Artemis as one coordinated change rather than two. A rotation applied on one side only stops inbound and outbound traffic at the same moment.
Use separate Twilio subaccounts or senders, credentials, webhooks, and connections for development and production.
***
## Manage the channel connection
Opening a connection shows four tabs. The delete control sits beside the connection name.
**Connection Summary** reports the connection's state and identity.
| Field | Description |
| - | - |
| **Status** | Whether the connection currently handles messages. |
| **Created** and **Updated** | Date and time of creation, and of the last change. |
| **Environment** | The environment whose deployment answers messages. |
| **Source Type** | Reads `Channel Connection`. |
| **Source ID** | Identifies the connection uniquely. Quote it when you raise a support ticket. |
**Setup Instructions** shows the **Webhook URL** to configure in Twilio and the **Connection Identifier** it ends with, each with a copy control. Compare the URL here with what Twilio actually calls whenever signature validation fails.
Artemis encrypts credentials after you save them, so each field reads as saved rather than showing its value. Leave a field empty to keep the current value, or enter a new one to replace it.
| Field | Description |
| - | - |
| **Use Auth Profile** | Draws the credentials from a saved auth profile instead of the fields below. Prefer it when the same credentials serve more than one connection, since a rotation then touches one profile. |
| **Account SID** | Replace it here if you move the sender to a different Twilio account or subaccount. |
| **Auth Token** | Replace it here the moment you rotate the token in Twilio, because inbound validation and outbound sends both stop until you do. |
| **Messaging Service SID** | Add or replace it to route replies through a service. Clear it to reply from the inbound number instead. |
Select **Save Credentials** to apply your changes.
Under **General**, rename the connection with **Display Name**, and revisit **Provider Verification Strength** if your trust posture for this sender changes.
Under **Twilio SMS Settings**, **Connection Identifier** appears read-only. The webhook URL is built from it, so changing it would invalidate the URL configured in Twilio. Create a new connection to serve a different sender.
**Authorization Message** controls what the person sees when a tool needs them to authorize something before the conversation continues. Write the template with these variables.
| Scope | Variables | Use |
| - | - | - |
| Message | `{{total}}`, `{{completed}}`, `{{remaining}}` | Summarize progress across every pending authorization, typically in a lead-in line above the list. |
| Repeat block | `{{#authorizations}}` and `{{/authorizations}}` | Wrap the section Artemis repeats once per pending authorization. |
| Item | `{{profileName}}`, `{{authLink}}`, `{{authUrl}}`, `{{index}}`, `{{count}}` | Identify and link each authorization inside the repeat block. |
Item variables resolve only inside the repeat block. Keep the wording short, because SMS renders it as plain text and long templates split into extra segments.
Select **Save Changes** to apply your changes.
Choose which deployed version of the agent answers messages on this connection.
Under **Agent Version**, set **Environment** to the environment you want the connection to follow. **Active Deployment** then shows the version currently serving traffic and when someone deployed it.
Check this pair when the channel connects but the agent behaves like an older build. The connection follows whatever the environment currently has deployed, not the version you last edited. Select **Save Changes** after you switch the environment.
***
## Troubleshoot connection issues
| Symptom | Likely cause | Recommended action |
| - | - | - |
| Artemis reports no connection found | Twilio is calling a URL that doesn't end with this connection's identifier, or the connection isn't active | Compare the URL configured in Twilio with the **Webhook URL** on the **Overview** tab, including the trailing identifier, and confirm the connection is Active. |
| The webhook returns 401 | The signed URL or the Auth Token doesn't match | Compare the exact external URL Twilio called with the public base URL you configured for Artemis, preserve `X-Twilio-Signature` through your ingress, and confirm the Auth Token belongs to the same Account SID. |
| The webhook returns 200 but no session appears | The request carried nothing to process, or the queue is unhealthy | Send a message with a non-empty body or at least one media item, then check queue health. |
| Inbound works but no reply goes out | The outbound credential, sender ownership, compliance state, or deployment is wrong | Verify the Account SID and Auth Token, confirm the account owns the sender, check destination eligibility and registration, and confirm the bound environment has a runnable deployment. |
| The Messaging Service sends from the wrong number or fails | The service belongs to another account, or the sender isn't in its pool | Confirm the `MG` SID belongs to the same account and the intended number sits in its sender pool. Clear the SID to reply directly from the inbound number. |
| SMS works but MMS fails | Capability, media access, or attachment policy blocks it | Confirm carrier and number support for MMS, that the Twilio media URL is reachable from an allowed host, that the credentials authenticate the download, and that content type and size pass your attachment policy. |
| Customers receive duplicate replies | Two inbound handlers point at this connection, or Twilio retried | Confirm only one number or Messaging Service handler targets this connection, and deduplicate on `MessageSid` to absorb Twilio's retries. |
| Long messages cost or render unexpectedly | Carrier segmentation split the message | Shorten agent replies, test representative Unicode content, and check per-segment behavior for the destination countries. |
***
## Twilio reference
Use these Twilio pages when you need detail beyond what this article covers.
| Resource | Use it for |
| - | - |
| [Twilio Console](https://www.twilio.com/console) | Account credentials, active numbers, and Messaging Services. |
| [Incoming message webhooks](https://www.twilio.com/docs/usage/webhooks/sms-webhooks) | The form fields Twilio posts and how the incoming-message webhook behaves. |
| [Messaging webhooks overview](https://www.twilio.com/docs/usage/webhooks/messaging-webhooks) | The difference between incoming-message webhooks and status callbacks. |
| [Security](https://www.twilio.com/docs/usage/security) | How Twilio builds the signature and what participates in it. |
| [Messaging Services](https://www.twilio.com/docs/messaging/services) | Sender pools, sticky sender behavior, and service-level inbound handling. |
| [A2P 10DLC](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc) | Brand and campaign registration for United States long-code traffic. |
**Related articles:**
* [Set up WhatsApp](/agent-platform/channels/whatsapp)
* [Set up Messenger](/agent-platform/channels/messenger)
* [Set up Instagram](/agent-platform/channels/instagram)
# Voice channels
Source: https://koreai.mintlify.app/agent-platform/channels/voice
[Back to voice channels](/agent-platform/channels#voice-channels)
## Set up Realtime LLM Voice channel
Supports live voice conversations using Realtime LLM models (OpenAI Realtime, Gemini Live) through the Kore.ai Voice Gateway or a Bring Your Own SIP connection.
To configure a Realtime LLM Voice channel, go to **Deployments** > **Channels** > **VOICE** > **Realtime LLM Voice** > **+ New** and complete the following steps in order.
On the **New Realtime LLM Voice Connection** page, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection. |
| **Connection Name** | A system name for the connection, such as `realtime-voice-prod`. Up to 255 characters. |
| **Environment** | The deployment the connection binds to. Select an environment to follow its active deployment automatically, or leave the default to use the working copy. When no environment has an active deployment yet, create a deployment first to bind the connection to a published environment. |
| **Provider Verification Strength** | Controls how strictly the platform trusts the provider's identity assertions for session continuity and linking. **Weak** is the default. Choose **Strong** only for channels or providers whose identity assertions you trust. |
| **Initial Status** | **Active** connections are usable immediately. **Inactive** connections stay saved but disabled until you activate them. |
Click **Create**.
After creating the channel, go to the **Configuration** tab. If you choose **Voice Provider: Kore.ai Voice Gateway**, set the following fields under **Kore.ai Voice Gateway**.
| Route | Description |
| - | - |
| **Soft Phone** (FREE) | Dial the agent from your browser with no DID purchase or telephony charges. Use it for evals, QA, and demos. **Launch dialer** opens the browser dialer. |
| **Phone Number (DID)** | Purchase a real phone number through Twilio. Disable this route to stop DID listing and call routing while keeping the number saved. |
If you select **Phone Number (DID)**, complete the next step. If you selected **Voice Provider: Bring Your Own SIP** instead, skip to Configure SIP Settings.
Set the **Provider** to **Twilio**, toggle **Inbound calls** (route calls from this number into the voice agent) and **Outbound calls** (allow this number to be used for campaign dialout), and select a **Country**, **Number Type**, and optional **Area Code**. Select **Search Available Numbers** to list numbers that match your filters.
Once the channel is created, select it, then go to the **Configuration** tab. If you chose **Voice Provider: Bring Your Own SIP**, set the following fields under **SIP Configuration**.
| Field | Description |
| - | - |
| **Our SIP Server Address** | Configure your SIP provider to send `INVITE` to this address, for example `korevg-np.kore.ai:5060`. Select **Copy** to copy the address. |
| **SIP Gateway IPs / CIDRs** | Provider SIP source addresses. Separate multiple IPv4 addresses or CIDR ranges with commas, for example `203.0.113.50:5080, 3.80.16.0/23`. |
| **SIP Termination URI** | Choose whether your SIP provider identifies by **IP Address** or **Domain Name**, then enter the SIP termination URI. |
| **DID / Phone Number** | The phone number (DID) your SIP provider routes to this SIP gateway. |
| **Enable E.164 (leading +)** | Prepend a leading `+` to outbound numbers sent to the carrier. On by default, which international (full-E.164) numbers require. Turn off only for local/national-format numbers — otherwise the leading `+` misroutes them. |
Under **Realtime LLM Voice settings**, set the following fields.
* **Realtime orchestration**: Select how runtime coordinates the conversation flow and business tool execution for realtime voice interactions. Realtime orchestration determines whether the realtime model or runtime controls conversation progression and business actions, such as bookings, orders, and payments.
| Mode | Description | Recommended for |
| - | - | - |
| **Legacy - model-selected tools** | Preserves the existing behavior where the realtime model manages both the conversation and business tool selection. | Existing agents and backward compatibility. |
| **Runtime Guarded - hybrid freeform (recommended)** | Allows the realtime model to conduct the conversation naturally while runtime validates tool execution and context updates against the current FLOW step. Business tools execute only when they belong to the active FLOW step, preventing out-of-order actions while maintaining the authored FLOW. | New realtime voice agents that require natural conversations while maintaining FLOW integrity. |
| **Runtime Owned - canonical turn** | Runtime controls every conversation turn and executes the FLOW exactly as authored. The realtime model handles conversational responses but cannot invoke business tools directly. | Workflows involving payments, bookings, orders, or other irreversible business actions and highly deterministic flows. |
* Changes to the orchestration mode apply only to new realtime sessions. Calls already in progress continue using the mode that was active when the session started.
* Existing channel configurations without a configured orchestration mode continue to use Legacy behavior.
* **Response wording owner**: Select who generates the wording of responses for realtime voice conversations. This setting is available only when the Realtime orchestration mode is set to **Runtime Owned — canonical turn**.
* **Runtime text (recommended)**: Runtime generates the response text. This option provides consistent response generation.
* **Provider-grounded voice**: Uses the realtime voice provider to generate spoken responses while runtime continues to control conversation flow and business actions. This option is available only for Gemini Live. For unsupported or unsafe conversation turns, it automatically falls back to runtime-generated text responses.
* **Semantic convergence**: Semantic Convergence determines how the platform coordinates with the realtime voice provider during a voice interaction. Controls whether realtime voice turns use the coordinated utterance runtime. This setting is available only when the realtime orchestration config is set to **Legacy mode**.
| Mode | Description | Recommended use |
| - | - | - |
| **Off** | Uses the legacy realtime voice execution model. The realtime provider executes tool calls directly without semantic coordination. | Use for backward compatibility with existing voice applications. |
| **Shadow** | Uses the legacy execution model while collecting diagnostics to compare legacy behavior with semantic convergence. Doesn't affect customer interactions. | Use for validation, testing, and migration before enabling **Enforce**. |
| **Enforce** | Uses semantic convergence to route voice turns through the platform's coordinator, ensuring tool execution and business logic are processed consistently with the agent runtime. | Recommended for new realtime voice deployments and production environments requiring consistent behavior across voice and text. |
* **S2S Provider**: The realtime speech-to-speech model the channel uses. Select one of the following: **Amazon Nova Sonic (S2S)**, **Azure OpenAI Realtime**, **Grok Realtime (S2S)**, **Google Gemini Live**, or **OpenAI Realtime**. Each option shows which credentials it uses:
| Provider | Credentials used |
| - | - |
| **Amazon Nova Sonic (S2S)** | Voice Service credentials (Amazon Nova Sonic (S2S) Credentials). |
| **Azure OpenAI Realtime**, **Grok Realtime (S2S)**, **Google Gemini Live**, **OpenAI Realtime** | The project-selected model connection. See **Realtime model connection** below. |
* **Realtime model connection**: Shown for every S2S provider except Amazon Nova Sonic, which authenticates via Voice Service credentials instead. Selects the project-pinned realtime model this provider uses - shows `Use project realtime model` until a workspace admin adds one:
1. Configure the model under [Voice Services](/agent-platform/administration/ai-configuration#voice-services), then add it to the [model catalog](/agent-platform/administration/ai-configuration#model-catalog) as an LLM provider.
2. In the project, go to **Settings** -> **Model** and add the model you configured at the workspace level.
* **Provider-specific settings**: After you select an S2S provider, a configuration section for that provider appears. Expand the provider below to see its settings.
Under **Amazon Nova Sonic Configuration**, set the following fields.
| Field | Description |
| - | - |
| **Voice** | The voice Nova Sonic uses to speak to callers. Default: **Tiffany**. |
| **Model** | The Amazon Bedrock model ARN, for example `arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-2-sonic-v1:0`. |
| **Maximum tokens** | The maximum number of tokens in a model response. Default: `1024`. |
| **Temperature** | Controls response randomness. Lower values give more predictable responses; higher values give more varied responses. Default: `0.70`. |
| **Top P** | Limits the model to the smallest set of likely tokens whose combined probability reaches this value. Default: `0.90`. |
| **Endpointing sensitivity** | How quickly Nova decides the caller has finished speaking. **Medium** (default) suits general conversation. **High** responds sooner but may interrupt callers. **Low** allows more pause before responding. |
**Pending tool fillers**
Turn on **Pending tool fillers** to let Nova speak a brief hold phrase when a project tool is still running. Fast tools finish without a filler.
| Field | Description |
| - | - |
| **Filler delay (milliseconds)** | Nova speaks only if the tool is still pending after this delay. Leave empty to use the default of `900` ms. |
| **Tool filler instruction** | The instruction Nova follows to generate the hold phrase. Use `{{toolName}}` to insert the name of the running tool. Default: `The tool {{toolName}} is still in progress. Briefly tell the caller that you are working on it.` |
**Coordinator turn filler**
Turn on **Coordinator turn filler** to let Nova speak a short hold phrase during convergence-enabled coordinator turns while Runtime decides and executes the turn.
| Field | Description |
| - | - |
| **Coordinator filler instruction** | The instruction Nova follows to generate the hold phrase. Default: `Briefly tell the caller that you are still working on their request.` Runtime sends this as native cross-modal input and keeps its transcript out of the canonical conversation history. |
**Advanced voice behaviour (Nova-specific)**
| Field | Description |
| - | - |
| **Failed tool call fallback** > **Tell the caller and end the call** | When a Nova tool call fails, Nova says a short apology and ends the call. Off by default. |
Also configure the **Advanced voice behaviour** settings that apply to all providers, described after the provider list.
Under **Azure OpenAI Realtime Configuration**, set the following fields.
| Field | Description |
| - | - |
| **Voice** | The voice used to speak to callers. Default: **Marin (recommended)**. |
| **Temperature** | Controls response randomness. Range: **Focused** (`0.6`) to **Creative** (`1.2`). Default: `0.8`. |
**Turn detection**
| Field | Description |
| - | - |
| **Speech detection sensitivity** | Adjusts how readily the model detects speech. Default: `0.5`. |
| **Silence Duration (ms)** | How long the caller must be silent before the turn is treated as complete. Default: `700`. |
| **Prefix Padding (ms)** | Audio kept before detected speech so the start of the caller's utterance is preserved. Default: `300`. |
Also configure the **Advanced voice behaviour** settings that apply to all providers, described after the provider list.
Under **Grok Realtime Configuration**, set the following fields.
| Field | Description |
| - | - |
| **Voice** | The voice used to speak to callers. Default: **Ara**. |
| **Temperature** | Controls response randomness. Range: **Focused** (`0.0`) to **Creative** (`2.0`). Default: `0.8`. |
| **Threshold** | Controls speech detection. Range: **Sensitive** (`0.0`) to **Conservative** (`1.0`). Default: `0.5`. |
| **Silence Duration (ms)** | How long the caller must be silent before the turn is treated as complete. Default: `500`. |
| **Prefix Padding (ms)** | Audio kept before detected speech so the start of the caller's utterance is preserved. Default: `300`. |
Also configure the **Advanced voice behaviour** settings that apply to all providers, described after the provider list.
Under **Gemini Live Configuration**, set the following fields.
| Field | Description |
| - | - |
| **Voice** | The voice used to speak to callers, for example **Puck — Upbeat**. |
| **Enable context window compression** | Lets Gemini compress older session context so long-running Live conversations can continue beyond the model's context window. Off by default. |
| **Temperature** | Controls response randomness. Range: **Deterministic** (`0.0`) to **Creative** (`2.0`). Default: `0.8`. |
**Tool response scheduling**
| Field | Description |
| - | - |
| **Tool response scheduling** | Controls how Gemini resumes after a final tool result. **Interrupt** responds immediately to the final result. **When idle — finish the current response first** (default) waits for the current response to finish. **Silent** retains the final result without speaking right away. Progress and filler updates always wait until Gemini is idle. |
| **Pending tool fillers** | Lets Gemini Live acknowledge long-running project tools while the original tool call remains pending. Off by default. |
**Realtime turn filler**
Separate from project Voice Fillers. Applies only to Google or Vertex realtime S2S calls while semantic convergence is set to **Enforce**.
| Field | Description |
| - | - |
| **Native turn filler** | Lets Gemini speak a short hold phrase while Runtime settles an enforced turn. It never applies to transfers, handoffs, or protected playback. |
| **Generative native filler** | Lets Gemini choose a short, contextual hold phrase in the current language instead of using the deterministic phrase pool. |
| **Generative filler prompt override** | Overrides the prompt Gemini uses to generate the hold phrase. Default: `Reply with a short, natural hold phrase in the current language.` |
**Filler delivery safeguards**
Shared by pending-tool and realtime turn fillers.
| Field | Description |
| - | - |
| **Settlement watchdog** | Time allowed for filler generation or a held final response to settle before Runtime releases the safe fallback. Accepted range: `1000`–`60000` ms. Leave empty to use the platform default of `10000` ms. |
**Voice activity detection**
Controls when Gemini starts listening to a caller turn and how long it waits before closing that turn.
| Field | Description |
| - | - |
| **Start Sensitivity** | Higher detects speech sooner but may trigger on noise. Lower waits for a stronger speech signal. Default: **Default**. |
| **End Sensitivity** | Higher ends turns sooner for lower latency. Lower allows longer pauses before Gemini responds. Default: **Default**. |
| **Silence Duration (ms)** | Milliseconds of non-speech before Gemini closes the caller turn. Increase to allow pauses; decrease for faster responses. Default: `100`. |
| **Prefix Padding (ms)** | Milliseconds of detected speech required before Gemini commits a speech start. Lower values catch shorter utterances but can increase false starts. Default: `20`. |
**Caller transcription**
| Field | Description |
| - | - |
| **Caller language** (Optional) | The language callers speak, such as `en-US`. If left empty, Gemini guesses the language on every turn, which can turn English into another script. The call's language setting, when there is one, takes precedence. |
**Silent-turn recovery**
| Field | Description |
| - | - |
| **Recover silent turns** | Re-prompts, then restarts, a Gemini session that stays silent when it owes the caller an answer. |
| **Recover unanswered caller turns only** | Limits recovery to caller turns that received no answer. Included automatically when **Recover silent turns** is on. If Gemini hears the caller but does not respond within 8 seconds, the Gemini session restarts with the caller's words, without re-prompting tool results. |
**Advanced voice behaviour (Gemini-specific)**
| Field | Description |
| - | - |
| **Silent resume recovery** > **Recover a silent resumed session** | Starts a fresh Gemini session when a resumed one connects but never speaks. Off by default. |
Also configure the **Advanced voice behaviour** settings that apply to all providers, described after the provider list.
Under **OpenAI Realtime Configuration**, set the following fields.
| Field | Description |
| - | - |
| **Voice** | The voice used to speak to callers. Default: **Marin (recommended)**. |
**Turn detection**
Tune how OpenAI detects when speech starts and when the caller has finished speaking.
| Field | Description |
| - | - |
| **Turn detection mode** | How OpenAI detects turns, for example **Server VAD — audio activity**. |
| **Speech detection sensitivity** | Range: **More sensitive** (`0.0`) to **More conservative** (`1.0`). Lower values pick up quieter speech; higher values reduce background-noise triggers. Default: `0.5`. |
| **Silence Duration (ms)** | Quiet time before OpenAI treats the caller turn as complete. Default: `700`. |
| **Prefix Padding (ms)** | Audio kept before detected speech so the beginning of the caller's utterance is preserved. Default: `300`. |
**Caller transcription**
How the caller's speech is written down. This text is stored as the caller's turn and is what the agent checks answers against.
| Field | Description |
| - | - |
| **Caller transcriber** | The transcription model, for example **Whisper (default)**. |
| **Caller language** (Optional) | The language callers speak, such as `en-US`. If left empty, the transcriber guesses the language on every turn, which can turn English into another script. |
| **Words to expect** | Brand names and terms callers use, for example `Acme, Rewards, email addresses spelled out letter by letter`. Used by Whisper and GPT-4o Transcribe. |
Never put a caller's own details in **Words to expect**. The transcriber would then hear them from anyone.
Also configure the **Advanced voice behaviour** settings that apply to all providers, described after the provider list.
* **Advanced voice behaviour (all providers)**: Optional behaviours for this channel. Each one is off, empty, or set to the earlier platform behaviour until you change it.
| Field | Description |
| - | - |
| **Caller wording** > **Wording a caller turn runs on** | Select the wording the agent runs a caller turn on: **Model's wording** (default), **Auto: prefer the caller's transcript**, or **Caller transcript**. |
| **Default transfer announcement** > **Line spoken before a transfer** (Optional) | The line the agent speaks before a transfer, for example `One moment while I connect you.` |
* **Noise isolation**: Applies server-side noise reduction to inbound call audio to filter out background voices and ambient noise.
* **Voice transfer and survey capabilities**: A read-only panel that shows which transfer and survey capabilities Runtime supports for the selected provider.
### Manage an existing realtime LLM voice channel
Open a channel from **Deployments** > **Channels** > **Voice** > **Realtime LLM Voice**. It contains three tabs: **Overview**, **Configuration**, and **Deployment**.
**Connection Summary** displays the following fields.
| Field | Description |
| - | - |
| **Status** | For example: `Active` |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | For example: `dev` |
| **Source Type** | Realtime LLM Voice Channel |
| **Source ID** | Unique identifier (UUID) of channel |
The channel includes the same **General** (Display Name), **Provider Verification Strength**, call routing, **Realtime LLM Voice Settings**, and provider-specific settings (which vary by the selected S2S provider) described in [Set up Realtime LLM Voice](#set-up-realtime-llm-voice-channel). You can edit these settings any time and with immediate effect.
The **Deployment** tab displays these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Development`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Displays the bound deployment version and when it was deployed. |
Click **Save Changes** after switching the environment/version.
## Set up Pipeline Voice channel
It's a voice channel that handles calls through separate speech-to-text, LLM, and text-to-speech stages, connected through Kore.ai Voice Gateway. Unlike Realtime LLM Voice, which uses a single speech-to-speech model, this pipeline lets you choose and configure the ASR (speech-to-text) and TTS (text-to-speech) providers independently.
Choose your voice provider before you begin - it determines which additional fields apply later in this setup.
To configure a Pipeline Voice channel, go to **Deployments** > **Channels** > **Voice** > **Pipeline Voice** > **+ New** and complete the following steps in order.
On the **New Pipeline Voice Connection** page, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection. |
| **Connection Name** | A system name for the connection, such as `pipeline-voice-prod`. Up to 255 characters. |
| **Environment** | The deployment the connection binds to. Select an environment to follow its active deployment automatically, or leave the default to use the working copy. When no environment has an active deployment yet, create a deployment first to bind the connection to a published environment. |
| **Provider Verification Strength** | Controls how strictly the platform trusts the provider's identity assertions for session continuity and linking. **Weak** is set by default. Choose **Strong** only for channels/providers whose identity assertions you trust for stronger continuity and linking decisions. |
| **Initial Status** | **Active** connections are usable immediately. **Inactive** connections stay saved but disabled until you activate them. |
Click **Create**.
After creating the channel, go to the **Configuration** tab. Under **Pipeline Voice Settings**, set **Voice Provider** to either **Kore.ai Voice Gateway** or **Bring Your Own SIP** — this determines which call routing fields appear next.
If you chose **Kore.ai Voice Gateway**, set the following fields under **Call routing**. Choose how calls reach this agent — enable Soft Phone for browser testing, or add a DID when this channel needs a real phone number.
| Route | Description |
| - | - |
| **Soft Phone** (FREE) | Dial the agent from your browser. No DID purchase or telephony charges. Use this for evals, QA, and demos. Select **Launch dialer** to open the browser dialer. |
| **Phone Number (DID)** | Purchase a real phone number through Twilio. Disable this route to stop DID listing and call routing while keeping the number saved. |
If you enable **Phone Number (DID)**, complete the next step. If you chose **Voice Provider: Bring Your Own SIP** instead, skip to Configure SIP Settings.
Set the **Provider** to **Twilio**, toggle **Inbound calls** (route calls from this number into the voice agent) and **Outbound calls** (allow this number to be used for campaign dialout), and select a **Country** and **Number Type**. Optionally enter an **Area Code** to filter available local numbers. Select **Search Available Numbers** to list matching numbers.
If you chose **Voice Provider: Bring Your Own SIP**, set the following fields under **SIP Configuration**.
| Field | Description |
| - | - |
| **Our SIP Server Address** | Configure your SIP provider to send `INVITE` to this address, for example `korevg-np.kore.ai:5060`. Select **Copy** to copy the address. |
| **SIP Gateway IPs / CIDRs** | Provider SIP source addresses. Separate multiple IPv4 addresses or CIDR ranges with commas, for example `203.0.113.50:5080, 3.80.16.0/23`. |
| **SIP Termination URI** | The termination address your SIP provider expects outbound calls to reach, as either an **IP Address** or **Domain Name**. Enter the URI in the field below the selected type. |
| **DID / Phone Number** | The phone number (DID) your SIP provider routes to this SIP gateway, for example `+14155551234` or `98492`. |
Toggle **Enable E.164 (leading +)** to prepend a leading `+` to outbound numbers sent to the carrier - on by default, which international (full-E.164) numbers require. Turn it off only for local/national-format numbers, since the leading `+` otherwise misroutes them.
Under **Speech Recognition**, set the following fields.
| Field | Description |
| - | - |
| **Provider** | The speech-to-text credential set to use, for example `Deepgram Speech Credentials`. |
| **Language** | The recognition language, for example `Multilingual`. |
| **STT Model** | The recognition model, for example `Nova 3 (latest, most accurate)`. |
| **Minimum STT confidence** | Reject transcripts below this confidence (0-1) and treat them as no-input. `0` disables the gate. Provider-relative - calibrate per channel. |
| **Custom vocabulary** | Add domain-specific terms to improve speech recognition. You can set the **Phrase** (for example `ACME, policy number`) and optional **Boost**. For **Emotech**, add **hotwords** (domain-specific words or phrases you want the speech recognition system to recognize more reliably) and set the **Hotword Weight**. A value from 5 to 10 is recommended. Clear the custom vocabulary list to disable hotwords. |
| **Multilingual speech recognition** | Choose the recognizer for each configured language. Enable **A different recognizer per language** to select the speech recognizer for each language. Add a language profile before configuring language-specific recognition. |
Under **Speech Synthesis**, set the following fields.
| Field | Description |
| - | - |
| **Voices by language** | Enable this option to configure separate text-to-speech settings for each language supported by the agent. When disabled, the channel uses a single TTS configuration for all languages. |
| **Language** | Select the language for which you want to create a TTS profile, then click **Add language**. |
| **Provider** | The text-to-speech credential set to use for the selected language, for example `ElevenLabs (TTS) Credentials` or `Deepgram Speech Credentials`. |
| **Voice** | The voice used for synthesized speech for the selected language, for example `Sarah - Mature, Reassuring, Confident - (american, young, female)`. |
| **Switch TTS for detected language** | When enabled, calls can change the TTS language, voice, and model after speech recognition detects a different language. |
* After you click **Add language**, the platform creates a dedicated TTS configuration section for that language. Configure the provider, voice, model, and any provider-specific settings independently for each language.
* Available fields vary by provider. Some providers expose additional settings such as TTS model, output format, speed, temperature, and voice tuning controls. Each language profile maintains its own provider configuration, allowing multilingual agents to use different voices and models for different languages.
If you selected an ElevenLabs voice, override playback and prosody for this channel connection under **ElevenLabs Voice Settings**.
| Field | Description |
| - | - |
| **TTS Model** | The synthesis model, for example `Eleven Multilingual v2`. |
| **Speed** | Adjusts speaking pace. `1.0` is normal; lower slows the voice down, higher makes it faster. Range: `0.7`–`1.2`. |
| **Stability** | Controls consistency across generations. Higher is more stable and less expressive; lower allows more variation. Range: `0`–`1`. |
| **Similarity boost** | Keeps the output closer to the selected voice. Higher values preserve more of the original voice character. Range: `0`–`1`. |
| **Style exaggeration** | Amplifies voice style and emotion. Higher values can sound more expressive but may increase latency or instability. Range: `0`–`1`. |
| **Speaker boost** | Enhances speaker similarity and clarity. Turning it off may reduce latency for some voices. |
Under **Preview Voice**, enter sample text (up to 500 characters) and select **Generate** to hear the configured voice before saving.
Set the following fields.
| Field | Description |
| - | - |
| **Barge-in** | Allow the caller to interrupt the agent while it is speaking. |
| **Speech Timeout (ms)** | Controls how long the speech provider waits through silence before finalizing the current speech segment. This does not combine separate final results. For example, `3000`. |
| **Transcript Grouping Timeout (ms)** | After the speech provider returns a final result, waits this long for another final result and combines the parts into one user input. Leave blank to process each final result immediately. For example, `1500`. |
| **Welcome Message** | The opening prompt the agent speaks, such as `Hello! How can I help you today?`. |
| **Noise isolation** | Apply server-side noise reduction to inbound call audio to filter out background voices and ambient noise. |
Under **Connect greeting** (audio), optionally play a clip as soon as the call connects, before the agent's first turn. If the project's greeting-audio library has no clips yet, the panel shows a note and an **Open Greeting Audio** link to add one.
Click **Save Changes** to apply the configuration.
### Manage an existing Pipeline Voice channel
Open a channel from **Deployments** > **Channels** > **Voice** > **Pipeline Voice**. It displays the following three tabs.
The **Connection Summary** displays these fields.
| Field | Description |
| - | - |
| **Status** | for example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | for example: `dev` |
| **Source Type** | `Channel Connection` |
| **Source ID** | Unique identifier (UUID) of the channel |
Contains the same **General** (Display Name, Provider Verification Strength), voice provider and call routing, **Speech Recognition**, **Speech Synthesis** (including ElevenLabs voice tuning and preview, where applicable), and **Conversation Controls** described in [Set up Pipeline Voice](#set-up-pipeline-voice) above, editable at any time. Changing any of these settings here updates the live channel immediately after **Save Changes**.
The **Deployment** tab displays these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** menu (for example: `Development`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Displays the bound deployment version and when it was deployed. |
Click **Save Changes** after switching the environment or the version.
# Set up Web SDK channel
Source: https://koreai.mintlify.app/agent-platform/channels/web-sdk-channel
[Back to SDK](/agent-platform/channels#sdk-channels)
Embed a chat widget in your website with a single script tag. Web SDK is a deployment channel that connects a chat and voice widget on your site to a specific agent deployment.
## Create a Web SDK channel
The Web SDK channel connects a browser-based chat or voice widget to your agent. It supports two authentication modes — **Anonymous / Public-key bootstrap** for client-only apps, and **Hosted token exchange** for setups where a backend can hold a server secret.
Choose your authentication mode before you begin — it determines which additional fields apply later in this setup.
To configure a Web SDK channel, go to **Deployments** > **Channels** > **SDK** > **Web SDK** > **+ New** and complete the following steps in order.
On the **New Web SDK Connection** page, provide the following fields.
| Field | Description |
| - | - |
| **Display Name** | A label for the connection (up to 255 characters). |
| **Environment** | The environment this channel follows; it tracks that environment's active deployment. |
| **Authentication** | How customer browsers bootstrap SDK sessions. Select **Anonymous / Public-key bootstrap** or **Hosted token exchange**. |
| Mode | When to Use |
| - | - |
| **Anonymous / Public-key bootstrap** | Browser or client app. Store the public `pk_*` key, runtime endpoint, and channel selection in client-side configuration. No ABL secret is required, so you provision nothing extra on the customer server. |
| **Hosted token exchange** | Customer backend. The backend stores a reveal-once `sk_*` server secret and issues one-time `bootstrapToken` values; the browser stores only the short-lived token, runtime endpoint, and project ID. |
If you select **Hosted token exchange**, complete the next step. Otherwise, skip to Select the Public SDK Key.
The **Token envelope policy** controls whether Hosted Exchange SDK bootstrap and session credentials are signed or encrypted.
| Option | Meaning |
| - | - |
| Use runtime default | Defers to the project-level default Hosted Exchange token envelope policy |
| Signed tokens | Issues signed bootstrap/session tokens only |
| Prefer encrypted tokens (`jwe_preferred`) | Uses encrypted (JWE) tokens when Runtime's encryption capability is ready; falls back to signed tokens otherwise |
| Require encrypted tokens (`jwe_required`) | Requires encrypted issuance and verification end to end; fails closed rather than returning a signed token |
Select the **Public SDK Key** the browser will use during SDK bootstrap, then select **Create**.
Once the channel is created, select it, then go to the **Configuration** tab to update the following fields:
| Field | Description |
| - | - |
| **Allowed Origins (CORS)** *(optional, both modes)* | One URL per line, for example, `https://example.com`, `https://*.example.com`. These origins are enforced for the public key linked to this SDK channel. |
| **Server secret** *(Hosted token exchange only)* | Studio generates the `sk_*` server secret automatically on creation and reveals it once. Copy and store it in your customer backend immediately, since it cannot be viewed again. **Server Secret Status**, **Server Secret Prefix**, and **Last Rotated** are shown afterward; use **Rotate server secret** to issue a new one — the previous secret stops working immediately. |
| **Customer-issued encrypted bootstrap** *(Hosted token exchange only, toggle, off by default)* | Accept customer-backend-issued JWE bootstrap tokens so sensitive and identity data can be encrypted before it reaches Runtime. |
Regardless of authentication mode, the same widget behavior settings apply, under **Web SDK Settings**.
| Field | Description |
| - | - |
| **Widget Mode** | `Chat Only`, `Voice Only`, or `Chat + Voice`. |
| **Features** | Toggle **Chat** and **Voice** independently. |
| **Show Agent Activity Updates** | Off by default. Displays transient handoff, delegate, and reasoning activity for this SDK channel. |
| **Theme** | Choose a preset (for example `Light`) or select **Add themes** to tune the widget to match your host site. |
| **Welcome Message** | The first message shown to users, for example "Hello! How can I help you today?" |
Click **Save Configuration**, then **Save Changes** to apply the configuration.
## Manage an existing channel
Opening a channel from **Deployments** > **Channels** > **Web SDK** shows four tabs: **Overview**, **Configuration**, **Deployment**, and **Testing**.
The **Connection Summary** shows these fields.
| Field | Description |
| - | - |
| **Status** | for example: *Active* |
| **Created** | Timestamp the channel was created |
| **Updated** | Timestamp of the last change |
| **Environment** | for example: `dev` |
| **Source Type** | `SDK Channel` |
| **Source ID** | Unique channel identifier (UUID) |
Contains the same **General** (Display Name), **Authentication**, and widget behavior settings described in [Create a Web SDK Channel](#create-a-web-sdk-channel) above, editable at any time. Changing the auth mode, token envelope policy, allowed origins, or public key here updates the live channel immediately after **Save Changes**.
The **Deployment** tab shows these fields.
| Field | Description |
| - | - |
| **Agent Version** | An **Environment** dropdown (for example: `Development`) that selects which deployed version handles incoming messages. |
| **Active Deployment** | Shows the bound deployment version and when it was deployed. |
Click **Save Changes** after switching the environment/version.
Select **+ Generate Link** under **Secure Preview Link** to create a secure, time-limited link to preview your widget. The link expires after 7 days.
Copy the **Embed Code** the tab provides, for example:
```html theme={null} theme={null}
```
1. Copy the above embed code.
2. Replace the API key placeholder with your actual key.
3. Paste the code before the closing `