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

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

ON_START properties

Template references in ON_START

You can reference named templates in the RESPOND value:
This renders the template named welcome from the agent’s TEMPLATES: block.

ON_START with tool call

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:
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.

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:
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: 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:
A top-level HANDOFF: <Agent> (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:
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 remote (A2A) target is always refused. A route WHEN that only a model could judge (natural language, intent.*) is never satisfied by the top-level ON_START: HANDOFF: (it has no branch IF:, so its route WHEN must be met from start data alone). A BRANCHES: entry is different: once its IF: selects the handoff, a route WHEN the start data genuinely can’t decide no longer blocks it — only a WHEN the start data can evaluate is applied, and a false one still blocks.
How it runs, by channel and start type:
  • Eager start (no user message yet — Web SDK/web-debug connect, pipeline voice answer, an identified agent-assist welcome): the welcome is delivered and the child is activated without running — its before_agent hook runs, but neither a FLOW child’s entry step nor a reasoning child’s model runs before the user says anything. The user’s first message is then forwarded to the child exactly as on a lazy start: a FLOW entry step with an extraction contract (extraction_hints, options, or a non-string type) takes its value from that message, a plain field asks its own question instead, and an ON_INPUT on the entry step takes the message as its input; a reasoning child answers it as its first request. Output order: the entry agent’s before_agent output, the welcome, the handoff announcement (when EXPERIENCE_MODE speaks one), the child’s before_agent output — the child’s own opener is no longer part of this list, because the child hasn’t run yet. A child that has answered no request yet stays active even if it returns (RETURN: true); ordinary return rules apply from the user’s first message on. A child that hands the very first message straight back (for example an off-topic question) ends there instead of parking, so a later route to it starts the child fresh. A start with no welcome and no child before_agent output sends nothing — on pipeline voice the caller hears only the welcome (if any) and the child stays silent until the caller speaks.
  • Lazy start (the first user 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 internally as a dsl_on_start_handoff trace event (executed, blocked, failed, call_failed, superseded_by_tool_hook, skipped_mode) with a reason and identifiers only — never customer data. This event isn’t one of Studio’s labeled Traces Explorer cards (unlike, say, a mid-conversation “Agent Handoff” or “Handoff Check” card) — look for it through a session trace export or the debug API if you need to confirm why a start-time handoff did or didn’t run. 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

Hook points

Hook action properties

Each hook supports the same set of action properties: 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.
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: <condition> with an indented RESPOND: "<text>" 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:
  • 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.

Conditional interaction-language actions (before_turn)

A before_turn hook can switch the session’s interaction language for the turn’s reply, its filler, and (on KoreVG voice without a voice language profile) the TTS voice, conditionally based on a WHEN: guard. A WHEN: whose indented body is SET_INTERACTION_LANGUAGE: or CLEAR_INTERACTION_LANGUAGE is a language branch. This is the same WHEN: mechanism as a respond branch, but with a language action instead of RESPOND:. A language directive written at hook level with no WHEN: is a catch-all.
Rules:
  • A value that is a BCP 47 tag (quoted or not) is a compile-time literal, canonicalized ("es-mx" becomes es-MX). A value path or {{template}} is resolved at runtime, like a hook SET value; a result that is not a language the channel supports is refused (with a trace) and the language stays as it was. Any other value is a compile error (E394 in .abl, E396 in YAML).
  • CLEAR_INTERACTION_LANGUAGE takes no value (E395). A trailing colon is fine.
  • One WHEN: body holds exactly one action: a RESPOND: (a reply branch) or one language action. Both in one body, or two language actions, drop the whole branch with HOOK_WHEN_BODY_MULTIPLE_ACTIONS. Write two separate WHEN: branches instead.
  • Language branches are tried in order. The first whose WHEN: holds wins, so the catch-all (no WHEN:) belongs last. One placed earlier warns with HOOK_LANGUAGE_CATCH_ALL_NOT_LAST.
  • This grammar is before_turn only. The same directives under a WHEN: in any other hook are a compile error (HOOK_LANGUAGE_ACTION_NOT_BEFORE_TURN from the parser; HOOK_LANGUAGE_ACTION_UNSUPPORTED_HOOK if a language action reaches compilation attached to a different hook by another path).
  • A WHEN: that is not valid CEL drops that action with the compiler warning HOOK_LANGUAGE_WHEN_INVALID_CEL. Unlike a respond branch, it never becomes unconditional.
  • YAML form: the hook carries interaction_language_actions, a list of entries with an optional when and exactly one of set_interaction_language, set_interaction_language_expression, or clear_interaction_language: true. Anything else is HOOK_LANGUAGE_ENTRY_INVALID.
Timing. The runtime evaluates language actions in an early pass, before the filler and before the model call, against the session as it stood at the start of the turn (including session.languageDetection). The hook’s own CALL and SET run later in the turn, so a language guard never sees values the hook itself just set. It sees what the previous turn left. The chosen language becomes the session’s interaction language and holds until another action changes or clears it. Gating. Language actions take effect only when the project’s language-detection setting is on (a project-level Studio setting, not declared in ABL), and only on chat and on voice without a voice language profile. Omitting the project setting while authoring this grammar produces a compiler reminder (HOOK_LANGUAGE_ACTIONS_REQUIRE_SETTING). Realtime speech-to-speech sessions never run them: their language is fixed by the provider session.

session.languageDetection shape

The session.languageDetection object holds the latest detection result. The project’s language-detection setting can configure an allowed-languages list and a minimum message length (default 12 characters). A message in a language outside the allowed list comes back no_language with reason: "not_allowed", so an unsupported language is never offered as candidate. A message shorter than the minimum comes back low_confidence with reason: "too_short" and its guess as candidate, whatever the confidence: a single short word like “Ciao” can still score 1.0. Scripts used by only one supported language (Korean, Japanese, Thai, Greek, Hebrew, and most Indic scripts) are exempt from the minimum-length rule. The session’s current interaction language is sent as a hint to the detection service, improving accuracy on short or romanized input.

Recipe: avoid language flapping

Short or code-mixed turns (Hinglish, for example) can flip between two detected languages turn to turn. Applying every detected result makes the reply, filler, and voice change each turn. Switch only on two consecutive matching detections of useful length. The hook SET records this turn’s detection for the next turn’s guard:

Recipe: FLOW agent with language switching

A FLOW agent takes the same before_turn actions. They run before the turn’s step. A model-written reply (a REASONING: true step) and fillers follow the chosen language. A fixed RESPOND: in a REASONING: false step is delivered as written and is never auto-translated. Localize it with {{locale.KEY}} from the project’s locale files: the language setters refresh the locale catalog in the same turn, so the step renders the text for the new language. In this example the English branch comes first (the first matching WHEN: wins), so a confident English message clears back to the default language while any other confident detection switches:

Example: audit logging hook

Example: turn timing

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

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:
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:

Error handler properties

Retry strategies

Then actions

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.

Step-level error handler properties

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.

Common message keys

On the Genesys Bot Connector specifically, setting escalation_format: "" (an explicit empty string, not omitting the key) suppresses the whole reply — text, buttons, and citations — on a successful terminal escalation; the configured escalation intent is still sent, so the customer’s session still escalates, it just gets no message. Omitting the key keeps the normal message behavior, and any nonempty value behaves as before. This only changes what the Bot Connector sends outward — it doesn’t affect the internal execution transcript, ongoing (non-terminal) replies, an unsuccessful escalation, or any other channel.
| 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_<operation> keys:
Add a _<language> 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_<operation>_<region-tag> → filler_<operation>_<language> → filler_<operation> → filler_general_<region-tag> → filler_general_<language> → 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: