Route between agents with handoff

In shortTransfer control between agents in a multi-agent graph using handoff tools and Command routing.

  • 6 min read
  • 10 sections
  • Updated
  • v0.10.0
  • Markdown

In a multi-agent graph, agents often need to delegate tasks to specialists. 10xGraph provides two patterns to route control: the handoff tool convention, where transfer_to_<agent> tools intercept the LLM’s tool calls and navigate the graph directly, and the Command mechanism, where a node function explicitly routes to another agent. Handoff tools keep the conversation history clean because the tool body never executes; Command routing gives you programmatic control over navigation and state updates.

When to use each pattern

Handoff tools (transfer_to_X) are best when agents decide where to go next via tool calls. The LLM decides “I need to ask the researcher” and calls the corresponding tool. The framework detects the tool name, intercepts it before execution, and routes to the target node. This is lightweight and requires minimal setup per agent.

Command routing works when a dedicated node (often a router or condition function) makes routing decisions based on message content or logic you define. Return Command(goto="TARGET_NODE") from any custom node function to navigate explicitly. This is useful when routing decisions come from parsing outputs, matching patterns, or applying complex rules that the LLM should not see.

If your agents are all peers (equal, collaborative) and you want automatic handoff tool injection, consider /docs/guides/prebuilt/swarm-agent instead. If you have a supervisor that delegates to workers, use /docs/guides/prebuilt/supervisor-team-agent. Use manual handoff when you need full control over the graph topology or custom routing.

How handoff tools work

When you call create_handoff_tool("researcher"), it returns a function named transfer_to_researcher with metadata flags that tell the graph this is a handoff. When an agent’s tool node receives this tool call, the node handler checks the tool name before executing. If it matches the transfer_to_* pattern, the handler extracts the target agent name, updates the routing, and skips the tool function body. The message history stays clean because no tool-result message is added.

Python
Agent LLM thinks: "I need the researcher to investigate."
    ↓
Agent calls tool: transfer_to_researcher
    ↓
Graph handler detects "transfer_to_" prefix
    ↓
Graph routes to the node named "researcher"
    (tool function never runs)

The graph does the routing, not the tool function. This keeps the conversation clean and makes routing transparent to the LLM.

Complete working example

Install with pip install "10xgraph[google-genai]" and set GEMINI_API_KEY (or GOOGLE_API_KEY). Here’s a fully runnable three-agent graph: a triage agent routes requests to a researcher or writer, and they can hand back to the triage agent or to each other.

Python
from tenxgraph.core import Agent, StateGraph, ToolNode, Message
from tenxgraph.core.state import AgentState
from tenxgraph.prebuilt.tools import create_handoff_tool
from tenxgraph.utils import END

# ────────────────────────────────────────────────────────────────────────────
# Helper: Check if the last message has tool calls
# ────────────────────────────────────────────────────────────────────────────
def has_tool_calls(state: AgentState) -> bool:
    """Return True if the last message contains tool calls."""
    return bool(state.context and state.context[-1].tools_calls)

# ────────────────────────────────────────────────────────────────────────────
# Tools: Handoff tools for each agent
# ────────────────────────────────────────────────────────────────────────────
triage_tools = ToolNode([
    create_handoff_tool("researcher", "Hand off to researcher for fact-finding."),
    create_handoff_tool("writer", "Hand off to writer for drafting."),
])

researcher_tools = ToolNode([
    create_handoff_tool("triage", "Return to triage coordinator."),
    create_handoff_tool("writer", "Hand off to writer with research findings."),
])

writer_tools = ToolNode([
    create_handoff_tool("triage", "Return to triage coordinator."),
    create_handoff_tool("researcher", "Ask researcher for more information."),
])

# ────────────────────────────────────────────────────────────────────────────
# Agents
# ────────────────────────────────────────────────────────────────────────────
triage_agent = Agent(
    model="gemini-2.5-flash",
    provider="google",
    system_prompt=[{
        "role": "system",
        "content": "You are a triage coordinator. Route user requests to the researcher for investigation or to the writer for content creation.",
    }],
    tool_node="triage_tools",
)

researcher_agent = Agent(
    model="gemini-2.5-flash",
    provider="google",
    system_prompt=[{
        "role": "system",
        "content": "You are a researcher. Investigate topics thoroughly. When done, hand off to the writer or return to triage.",
    }],
    tool_node="researcher_tools",
)

writer_agent = Agent(
    model="gemini-2.5-flash",
    provider="google",
    system_prompt=[{
        "role": "system",
        "content": "You are a writer. Draft content based on the materials provided. Ask for more info if needed, or return to triage.",
    }],
    tool_node="writer_tools",
)

# ────────────────────────────────────────────────────────────────────────────
# Graph
# ────────────────────────────────────────────────────────────────────────────
graph = StateGraph()

# Add agent nodes
graph.add_node("triage", triage_agent)
graph.add_node("triage_tools", triage_tools)
graph.add_node("researcher", researcher_agent)
graph.add_node("researcher_tools", researcher_tools)
graph.add_node("writer", writer_agent)
graph.add_node("writer_tools", writer_tools)

# Set entry point
graph.set_entry_point("triage")

# Tool nodes route back to their agents
graph.add_edge("triage_tools", "triage")
graph.add_edge("researcher_tools", "researcher")
graph.add_edge("writer_tools", "writer")

# Agent routing: if tool call exists, go to tool node; otherwise end
graph.add_conditional_edges(
    "triage",
    lambda state: "triage_tools" if has_tool_calls(state) else END,
    {"triage_tools": "triage_tools", END: END},
)

graph.add_conditional_edges(
    "researcher",
    lambda state: "researcher_tools" if has_tool_calls(state) else END,
    {"researcher_tools": "researcher_tools", END: END},
)

graph.add_conditional_edges(
    "writer",
    lambda state: "writer_tools" if has_tool_calls(state) else END,
    {"writer_tools": "writer_tools", END: END},
)

app = graph.compile()

# ────────────────────────────────────────────────────────────────────────────
# Run it
# ────────────────────────────────────────────────────────────────────────────
result = app.invoke({
    "messages": [Message.text_message("Find recent AI breakthroughs and write a summary article.")]
})

for msg in result["messages"]:
    print(f"{msg.role}: {msg.text()}")

The flow: triage receives the request, decides to call transfer_to_researcher to investigate. The graph intercepts this tool call, routes to the researcher node, and the researcher runs. When the researcher calls transfer_to_writer, the graph routes there. When the writer finishes (no handoff called), execution ends.

Alternative: explicit routing with Command

If you prefer to control routing in code rather than via tool calls, return a Command from a custom node:

Python
from tenxgraph.utils import Command, END
from tenxgraph.core.state import AgentState

def router_node(state: AgentState, config: dict) -> Command:
    """Route based on message content."""
    last_msg = state.context[-1].text() if state.context else ""

    if "research" in last_msg.lower():
        return Command(goto="researcher")
    elif "write" in last_msg.lower():
        return Command(goto="writer")
    else:
        return Command(goto=END)

graph.add_node("router", router_node)
graph.set_entry_point("router")

Command lets you:

  • goto: Route to any node name or END
  • update: Apply state changes before navigating (e.g., add a message)
  • graph: Return to a parent graph with Command.PARENT
  • state: Attach a full state object

Command routing is useful when decisions come from parsing outputs, applying business logic, or orchestrating multi-turn flows where the LLM should not see routing logic.

Return to a parent graph

In a nested graph scenario, signal that you’re done with the subgraph:

Python
# In a subgraph node
return Command(goto=END, graph=Command.PARENT)

This hands control back to the parent graph’s routing, not to END in the subgraph.

Routing topologies

Peer-to-peer

All agents can call all others. Each agent has handoff tools for every peer:

plaintext
Agent A ←→ Agent B
  ↕         ↕
Agent C ←→ Agent D

Use SwarmAgent to automate this: it generates all transfer_to_* tools and injects them.

Hub and spoke

One central coordinator routes to specialist agents, and specialists return to the coordinator:

plaintext
        ┌─ Researcher
        │
Triage ─┼─ Writer
        │
        └─ Analyst

Use SupervisorTeamAgent for this pattern: it generates a supervisor LLM that decides where to route.

Chain

Agents form a linear sequence; each hands off to the next:

plaintext
Investigator → Analyzer → Writer → Editor → END

Use manual handoff with conditional edges that route sequentially.

Common errors and fixes

Error Cause Fix
Agent keeps calling transfer_to_X but never moves. Target node name doesn’t exist in the graph. Verify the agent_name argument to create_handoff_tool() matches an add_node() call exactly (case-sensitive).
Handoff tool executes (logs show “should have been intercepted”). Node handler is not the framework’s built-in handler; you’ve replaced it with custom logic. Use Agent and ToolNode directly; do not override node handlers.
Routing loop: agents keep handing off to each other infinitely. No base-case conditional edge to END. Add END as an option from at least one agent’s conditional edges. Ensure an agent can finish without calling a handoff.
Command.goto is ignored. Command returned from an Agent node instead of a custom function. Return Command only from custom node functions, not from Agent instances. Agents return messages.
Handoff to agent X works but state looks wrong. State not passed through the handoff correctly. The framework preserves state.context (messages) across handoffs. If you need custom state, subclass AgentState and use reducers.

How to verify

After building the graph, compile it and run with a simple input to trace the path:

Python
import logging
logging.basicConfig(level=logging.DEBUG)

result = app.invoke({"messages": [Message.text_message("Your request")]})

The logs show which nodes execute in order, making it easy to confirm the routing is working.

  • /docs/guides/prebuilt/swarm-agent: Automate peer-to-peer handoff for multi-agent teams.
  • /docs/guides/prebuilt/supervisor-team-agent: Use a supervisor LLM to route work to specialists.
  • /docs/guides/build-a-graph: Fundamentals of building graphs and routing.
  • /docs/concepts/state-graph: How StateGraph, nodes, and edges work.
  • /docs/concepts/agents-and-tools: Agent behavior and tool dispatch.

Frequently asked questions

When should I use this instead of SwarmAgent?
Use manual handoff when you need full control over graph topology or custom routing logic. Use SwarmAgent when all agents are peers and you want automatic handoff tool injection.
Can handoff tools route to agents that do not exist?
The handoff is detected and routed before tool execution, so the target node must exist in the graph. A non-existent target causes a routing error.
How do I prevent routing loops?
Ensure at least one agent has a conditional edge that routes to END when no handoff is called, breaking the cycle.
Last updated for v0.10.0Edit this page on GitHubReport an issue