Concept
A permanent handoff setsRETURN: false on a HANDOFF route. RETURN: false is the default. A route with no RETURN line is permanent. EXPECT_RETURN: false is the same setting.
When the route fires, ownership moves in four steps.
- The calling agent thread ends.
- The target becomes the active agent.
- The target answers this turn and every later turn.
- No value returns to the caller, and
ON_RETURNnever runs.
HANDOFF entries when it must switch again.
See Switch the active agent for how
HANDOFF changes the active agent. See Returnable handoff when the caller must continue. See Use handoff instead of delegate to choose between HANDOFF and DELEGATE.
Route settings
HISTORY accepts full, auto, none, summary_only, and a last_n block. auto uses summary_only when SUMMARY has text. Otherwise it passes the last five messages. A project default can change the full default.
ON_FAILURE accepts continue, ESCALATE, and RESPOND "message". It runs when the runtime cannot start the handoff. It does not run when the target later fails on its own.
Hand-back to the supervisor
A permanent specialist can hand the conversation back to the supervisor that started it. Add aHANDOFF to that supervisor in the specialist. These rules apply.
- Only the direct parent qualifies. An older ancestor does not.
- The specialist must have its own
HANDOFFto the parent. - The
ENABLED_WHENof that route still applies. - A parent that already took part in the same call chain stays blocked. The runtime refuses it as a cycle.
- For a specialist with a
FLOW, the hand-back works only while theFLOWis live. After theFLOWreachesCOMPLETE, the parent frame is closed.
handoff_completed outcome: rejected and code: recursion. The trace can also show handoff_authority_denied.
Handoff to a human agent
AHANDOFF target is always an agent in the project. It is never a person. To reach a human, use one of these paths.
See Escalate a conversation to a human for triggers and routing. See Package context for human escalation for what the human agent receives.
Minimal working example
The supervisor sends disputes toDisputes_Agent and requests for a person to Live_Agent. Both routes are permanent. Disputes_Agent can hand back to the supervisor.
customer_id is a project-local assumption. Your channel or an earlier gate sets it before the handoff. Live_Agent is a short stand-in. Its ESCALATE section sets what the human agent receives.
How it works
- The customer reports a charge problem. The supervisor classifies the intent as
dispute. ENABLED_WHENis true unless the caller sets_metadata.disputes_enabledto false. The route is on.- The route fires. The supervisor thread ends.
Disputes_Agentreceivescustomer_id, the summary, and the full history. Disputes_Agentasks for the charge ID. Later messages go to it and not to the supervisor.- The
COMPLETEcondition is true whencharge_idis set. The agent confirms the dispute. - If the customer asks about something else, the model can use the route back to
Card_Supervisor. The quotedWHENis guidance. The model decides.
WHEN and ENABLED_WHEN rules apply to a permanent route like any other route. ENABLED_WHEN decides whether the route exists. WHEN decides which available route a turn takes. A // outside quotes in a WHEN starts a comment. Use IS SET and IS NOT SET for empty variables. The check != "" is true when the value is missing.
A route written without WHEN compiles without a warning. Write a WHEN on every route so the selection stays predictable.
Common variations
Write the context as shorthand
PASS, SUMMARY, and HISTORY can sit directly on the route, as the Live_Agent route shows. The CONTEXT: block groups the same values. Keys inside CONTEXT: must be lowercase. Uppercase keys inside CONTEXT: are ignored.
Write a last_n history as an indented block.
{ mode: last_n, count: 6 } and a misspelled value such as recent are ignored. The history falls back to full.
Turn a route off for some sessions
ENABLED_WHEN reads only data known before the turn, such as _metadata.*. It cannot read intent. A condition that reads a missing value turns the route off, unless it uses != true, == null, or IS NOT SET. When the route is off, the ON_FAILURE of the entry runs for an authored route. See the first route in the minimal example.
Do not use ENABLED_WHEN as a security boundary. The caller supplies _metadata.
Hand back from the specialist
TheDisputes_Agent entry in the minimal example is the hand-back. It targets the direct parent, so it qualifies. Add ENABLED_WHEN to that route to switch it off for some sessions. If every route in the specialist has ENABLED_WHEN, the compiler warns ENABLED_WHEN_ALL_DISABLED_RISK. The specialist then answers directly when all conditions are false.
Verification
- Validate the project. Every
TOtarget must exist. - Start a new session and send “I want to dispute a charge”. Expect a
handoffevent toDisputes_AgentwithreturnExpected: falseandreasonCode: handoff_permanent. - Expect a
handoff_completedevent withoutcome: successanddurationMs. - Send another message. Expect
Disputes_Agentto answer. The supervisor does not run. - Send “I want to talk to a person” in a new session. Expect a handoff to
Live_Agent. - In
Disputes_Agent, send an unrelated request. Expect a handoff toCard_SupervisorwithreturnExpected: false. - Send
_metadata.disputes_enabledas false. Expect the supervisor not to offer the route. A forced attempt showsoutcome: blocked.
The event also has
cause and code on a failure, and siblingIntentMs when a queued sibling request ran inside the attempt.
Production readiness checklist
- Every
TOtarget exists. Replace the stand-in agents with real agents. - The target has its own exit: a
COMPLETEcondition, anESCALATEsection, or aHANDOFF. A permanent target without an exit keeps the conversation. - Variables in
PASShave a source.customer_idhere comes from your channel or an earlier gate. - Each route sets
ON_FAILUREon purpose. - A human path exists, and the human agent receives the context it needs.
- Tests cover each route, a route that is switched off, a hand-back, a refused hand-back, and a message that matches no route.
- Trace checks cover
handoffandhandoff_completed.