Skip to content

Handoff (Transfer of Control)

The handoff pattern lets agents pass control of a conversation to each other. The agent that receives the handoff answers the user directly. It does not report back to the agent that transferred.

A handoff uses a tool, the same mechanism as the OpenAI Swarm and Agents SDK:

  1. UC AI runs the configured initial agent, for example a triage agent
  2. For every other agent in the handoff mesh, UC AI registers a temporary transfer_to_<agent> tool for that call. The agent keeps its own tools also
  3. When the AI decides that another agent must take over, it calls the transfer tool with a context summary
  4. UC AI ends the turn of that agent and runs the target agent. It passes the original input, the transfer context, and the conversation trail
  5. The target agent can answer, or it can transfer again. Every hop sees the transfer tools of all other agents in the mesh
  6. When an agent answers without a call to a transfer tool, its answer is final

max_handoffs bounds the length of the chain. The hop that reaches the cap runs without transfer tools, so it must answer. The chain therefore ends with an answer and not with an error.

OrchestratorHandoff
Control flowCentral agent calls specialists and gets their results backControl moves to the specialist, and it answers directly
Final answerThe orchestrator writes itThe last agent in the chain writes it
Specialist toolsSpecialists run as sub-agents with their own toolsThe same, and each hop also gets the transfer tools of the mesh
Good forCombining multiple sources into one planRouting to the one correct expert

A triage agent is the single point of entry. It hands off to a product specialist, a shipping specialist, and a customer account specialist. The product specialist has its own catalog lookup tool.

In a real system, each specialist queries real data with tools. This example holds the shipping data and the customer data in the system prompts, to stay simple. The product agent uses a real tool.

DECLARE
l_profile_id NUMBER;
BEGIN
-- Triage: routes, answers only smalltalk itself
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'support_triage_profile',
p_description => 'Support triage agent',
p_system_prompt_template => 'You are the triage agent of an online shop''s customer support.
You can transfer the conversation to specialist agents using the available transfer tools.
For any question about products, shipping, or customer accounts you MUST transfer to the
matching specialist - never answer such questions yourself.
Only greetings and smalltalk you answer yourself, briefly and politely.',
p_user_prompt_template => '{prompt}',
p_provider => uc_ai.c_provider_openai,
p_model => uc_ai_openai.c_model_gpt_5_6_luna,
p_status => uc_ai_prompt_profiles_api.c_status_active
);
-- Product specialist: uses a catalog lookup tool (register it via
-- uc_ai_tools_api with the tag "product_catalog" beforehand)
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'support_product_profile',
p_description => 'Product details specialist',
p_system_prompt_template => 'You are the product specialist of an online shop.
Use the product details tool to look up product facts and answer concisely.',
p_user_prompt_template => 'Customer question: {prompt}
Triage notes: {handoff_context}',
p_provider => uc_ai.c_provider_openai,
p_model => uc_ai_openai.c_model_gpt_5_6_luna,
p_model_config_json => '{"g_enable_tools": true, "g_tool_tags": ["product_catalog"], "g_max_tool_calls": 3}',
p_status => uc_ai_prompt_profiles_api.c_status_active
);
-- Shipping specialist
l_profile_id := uc_ai_prompt_profiles_api.create_prompt_profile(
p_code => 'support_shipping_profile',
p_description => 'Shipping specialist',
p_system_prompt_template => 'You are the shipping specialist. Shipping rules:
- Standard: 4 business days, $4.99
- Express: 1 business day, $19.99
- Orders over $100 ship free (standard)
Answer concisely with exact numbers.',
p_user_prompt_template => 'Customer question: {prompt}
Triage notes: {handoff_context}',
p_provider => uc_ai.c_provider_openai,
p_model => uc_ai_openai.c_model_gpt_5_6_luna,
p_status => uc_ai_prompt_profiles_api.c_status_active
);
COMMIT;
END;
/

Every agent in a handoff mesh must be an active profile agent.

DECLARE
l_agent_id NUMBER;
BEGIN
l_agent_id := uc_ai_agents_api.create_agent(
p_code => 'support_triage',
p_description => 'Support triage agent',
p_agent_type => uc_ai_agents_api.c_type_profile,
p_prompt_profile_code => 'support_triage_profile',
p_status => uc_ai_agents_api.c_status_active
);
l_agent_id := uc_ai_agents_api.create_agent(
p_code => 'support_product',
p_description => 'Product details specialist',
p_agent_type => uc_ai_agents_api.c_type_profile,
p_prompt_profile_code => 'support_product_profile',
p_status => uc_ai_agents_api.c_status_active
);
l_agent_id := uc_ai_agents_api.create_agent(
p_code => 'support_shipping',
p_description => 'Shipping specialist',
p_agent_type => uc_ai_agents_api.c_type_profile,
p_prompt_profile_code => 'support_shipping_profile',
p_status => uc_ai_agents_api.c_status_active
);
COMMIT; -- agents must be committed before the handoff agent references them
END;
/

The AI sees the description of each entry on its transfer tool. Write this description as a routing instruction. Include the triage agent in handoff_agents, so that a specialist can transfer back.

DECLARE
l_handoff_id NUMBER;
l_config CLOB;
BEGIN
l_config := '{
"pattern_type": "handoff",
"initial_agent_code": "support_triage",
"handoff_agents": [
{"agent_code": "support_triage", "description": "Triage and general support - transfer back for questions outside your specialty"},
{"agent_code": "support_product", "description": "Product details: specs, prices, availability of catalog products"},
{"agent_code": "support_shipping", "description": "Shipping: options, costs, delivery times"}
],
"max_handoffs": 3
}';
l_handoff_id := uc_ai_agents_api.create_agent(
p_code => 'customer_support',
p_description => 'Customer support entry point',
p_agent_type => uc_ai_agents_api.c_type_handoff,
p_orchestration_config => l_config,
p_status => uc_ai_agents_api.c_status_active
);
COMMIT;
END;
/
DECLARE
l_result json_object_t;
BEGIN
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'customer_support',
p_input_parameters => json_object_t('{"prompt": "How fast is your fastest shipping option?"}'),
p_session_id => uc_ai_agents_api.generate_session_id
);
DBMS_OUTPUT.PUT_LINE('Answer: ' || l_result.get_clob('final_message'));
DBMS_OUTPUT.PUT_LINE('Answered by: ' || l_result.get_string('final_agent_code'));
DBMS_OUTPUT.PUT_LINE('Handoffs: ' || l_result.get_number('handoff_count'));
END;
/

The triage agent recognizes a shipping question, calls transfer_to_support_shipping with a context summary, and the shipping specialist answers the user directly.

FieldTypeDescription
pattern_typeStringMust be "handoff"
initial_agent_codeStringAgent that receives every new conversation (must be an active profile agent)
handoff_agentsArrayTransfer targets: {"agent_code", "description", "can_transfer_to"?}
can_transfer_toArrayOptional for each entry: the agent codes that this agent can transfer to. Without it, the agent can transfer to every other entry (a full mesh)
max_handoffsNumberMaximum transfers per turn (default 3). The hop at the cap runs without transfer tools
history_managementObjectOptional trail trimming: {"strategy": "sliding_window"|"summarize"|"full", "max_messages": 20}

can_transfer_to restricts the outgoing edges of each agent. With it, you can build a hierarchy. The entry agent then sees only the department level, and each department delegates down to its own specialists:

Customer Service Agent
├── Product Support ──────┬── Product A Technician
│ └── Product B Technician
├── Shipping & Returns ───── Return Policy Specialist
└── Customer Details
{
"pattern_type": "handoff",
"initial_agent_code": "support_triage",
"handoff_agents": [
{"agent_code": "support_triage", "description": "Triage and general support",
"can_transfer_to": ["support_product", "support_shipping", "support_customer"]},
{"agent_code": "support_product", "description": "Products: specs, prices, technical problems",
"can_transfer_to": ["tech_product_a", "tech_product_b", "support_triage"]},
{"agent_code": "support_shipping", "description": "Shipping, delivery, returns",
"can_transfer_to": ["return_policy", "support_triage"]},
{"agent_code": "support_customer", "description": "Customer accounts and orders",
"can_transfer_to": ["support_triage"]},
{"agent_code": "tech_product_a", "description": "Technical problems with Product A",
"can_transfer_to": ["support_triage"]},
{"agent_code": "tech_product_b", "description": "Technical problems with Product B",
"can_transfer_to": ["support_triage"]},
{"agent_code": "return_policy", "description": "Return window, fees, refunds",
"can_transfer_to": ["support_triage"]}
],
"max_handoffs": 5
}

A technical question now travels from triage to product support to the technician. The technician answers the user directly. Every level keeps a back-edge to triage, so the conversation never stops inside a specialist. UC AI validates the edges at create_agent time. Each can_transfer_to code must name another handoff_agents entry.

A handoff agent supports a follow-up message with a sticky active agent. The next user message goes to the agent that answered the previous turn. This agent keeps its transfer tools. When the topic changes, it can hand off again.

DECLARE
l_result json_object_t;
l_session_id VARCHAR2(100);
BEGIN
l_session_id := uc_ai_agents_api.generate_session_id;
-- Turn 1: routed triage -> shipping specialist
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'customer_support',
p_input_parameters => json_object_t('{"prompt": "How much does standard shipping cost?"}'),
p_session_id => l_session_id
);
-- Turn 2: resumes directly with the shipping specialist (no re-routing)
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'customer_support',
p_follow_up_message => 'And how much is the express option?',
p_session_id => l_session_id
);
DBMS_OUTPUT.PUT_LINE(l_result.get_clob('final_message'));
END;
/

The resumed agent continues its own conversation history from the previous turn. If it transfers, the receiving agent gets the follow-up message in its prompt parameter, with the usual handoff_context. If the mesh no longer holds the previous final agent, the turn falls back to the initial agent.

A handoff run returns these fields, next to the standard fields final_message, execution_id, and status:

FieldDescription
final_agent_codeThe agent that produced the final answer
handoff_countNumber of transfers in this turn
handoff_trailOne array entry for each transfer: {hop, from_agent, to_agent, reason?, context}
conversation_historyArray of {agent, response} for every hop
max_handoffs_reachedtrue when the chain reached max_handoffs

Each hop runs as a child execution under the handoff wrapper. The full chain is therefore visible in uc_ai_agent_executions, through parent_execution_id. The transfer tool calls appear in the uc_ai_agent_messages log, with role = 'tool_call' and a tool_name like transfer_to_%. UC AI records the token usage on the hop executions, and totals it on the session header. The wrapper itself reports zero tokens of its own.

The uc_ai_agent_messages transcript records the whole chain: the user question, each transfer tool call, and the answer of the specialist. The transfer_to_* value in tool_name names the transfer target. agent_code names the producer of each row, so you can see which specialist spoke.

seqroleagent_codetool_namecontent
1user(null)Where is my order #12345?
2tool_calltriage_agenttransfer_to_shipping_agent
3tool_resulttriage_agenttransfer_to_shipping_agentTransferred to shipping_agent
4assistantshipping_agentYour order shipped yesterday...
  • Descriptions are routing rules: the description in handoff_agents becomes the description of the transfer tool. Write it as an instruction for the moment to transfer.
  • Tell the entry agent to transfer: instruct the triage agent to never answer a specialist question itself. Without this instruction, a capable model guesses an answer instead of a transfer.
  • Tell mid-level agents to delegate down: in a hierarchy, instruct each department agent to transfer a technical question to its specialists. An example instruction is “if transfer tools are available and the problem is technical, transfer to the matching technician”.
  • Use a capable model for the entry agent: the triage decision is a tool-calling task. A small model transfers less reliably.
  • Always give a back-edge: give every specialist a can_transfer_to edge back to the entry agent. A conversation then cannot stop outside the scope of an agent.
  • Specialists keep their tools: UC AI merges the transfer tools with the own tool configuration of the agent. A specialist can therefore read data and transfer back.
  • Loops are bounded, not detected: a reverse edge makes an A→B→A loop possible inside one turn. max_handoffs caps this loop, and the last hop must answer.