Handoff
In shortBuild a multi-agent 10xGraph system where agents transfer work to each other using handoff tools.
- 5 min read
- 6 sections
- Updated
- v0.10.0
- Markdown
A handoff lets an agent pass control to another agent by calling a tool named transfer_to_<name>. This example wires three agents (coordinator, researcher, writer) that delegate to each other with no fixed pipeline. The model chooses who works next, and the graph jumps there.
The source is agentflow/examples/handoff/handoff_multi_agent.py in the repository.
Run the example
You need the Google GenAI extra and a Gemini key. The example loads a .env file with python-dotenv, so install that too, or export the key in your shell.
pip install "10xgraph[google-genai]" python-dotenv
export GEMINI_API_KEY=your-key-here # GOOGLE_API_KEY also works
python agentflow/examples/handoff/handoff_multi_agent.pyThe script sends “Please research quantum computing and write a brief article about it.” and prints the full message history, including every tool call. Model output varies between runs.
How the handoff works
A handoff tool is a normal tool whose name starts with transfer_to_. Before a tool node runs any tool calls, it checks each name for that prefix. On a match it does not run the tool. It returns a Command whose goto is the text after the prefix.
Walk through the code
The code below is the example condensed into one runnable file. It follows the repository file, except that the handoff targets use the uppercase node names.
Imports and checkpointer
The imports bring in the agent, graph and tool node classes, the handoff helper and an in-memory checkpointer.
from dotenv import load_dotenv
from tenxgraph.core import Agent, StateGraph, ToolNode
from tenxgraph.core.state import AgentState, Message
from tenxgraph.prebuilt.tools import create_handoff_tool
from tenxgraph.storage.checkpointer import InMemoryCheckpointer
from tenxgraph.utils.constants import END
load_dotenv()
checkpointer = InMemoryCheckpointer()Define the regular tools
Each specialist gets one ordinary tool. They return canned strings so the example needs no external services.
def get_weather(location: str) -> str:
"""Get the current weather for a specific location."""
return f"The weather in {location} is sunny, 25C"
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Search results for '{query}': Found relevant information about quantum computing"
def write_document(content: str, title: str) -> str:
"""Write a document with the given content and title."""
return f"Document '{title}' written successfully with {len(content)} characters"The repository version also declares optional tool_call_id and state parameters on these tools. They are injected at call time and hidden from the model, which is useful for logging but not needed here.
Give each agent a tool node with handoff tools
Each agent’s tool node mixes its own tools with create_handoff_tool calls for the agents it may pass work to. The second argument is the description the model sees.
# Coordinator delegates to either specialist and can check the weather.
coordinator_tools = ToolNode(
[
create_handoff_tool("RESEARCHER", "Transfer to research specialist for detailed investigation"),
create_handoff_tool("WRITER", "Transfer to writing specialist for content creation"),
get_weather,
]
)
# Researcher searches, then hands to the writer or back to the coordinator.
researcher_tools = ToolNode(
[
search_web,
create_handoff_tool("COORDINATOR", "Transfer back to coordinator for delegation"),
create_handoff_tool("WRITER", "Transfer to writer with research findings"),
]
)
# Writer saves a document, then hands back to the coordinator.
writer_tools = ToolNode(
[
write_document,
create_handoff_tool("COORDINATOR", "Transfer back to coordinator"),
]
)Create the three agents
Each Agent has a system prompt and the name of its tool node. The prompt tells the model which handoff tools exist and when to use them.
coordinator_agent = Agent(
model="gemini-2.5-flash",
provider="google",
system_prompt=[
{
"role": "system",
"content": """You are a coordinator agent. Your job is to:
1. Understand user requests
2. Delegate tasks to specialized agents:
- Use transfer_to_RESEARCHER for investigation and research tasks
- Use transfer_to_WRITER for content creation and writing tasks
3. You can also check weather using get_weather
Always explain your decision to delegate.""",
},
],
tool_node="COORDINATOR_TOOLS",
trim_context=True,
)
researcher_agent = Agent(
model="gemini-2.5-flash",
provider="google",
system_prompt=[
{
"role": "system",
"content": """You are a research specialist. Investigate topics with search_web.
When research is complete, use transfer_to_WRITER if content must be written,
or transfer_to_COORDINATOR if the task is done.""",
},
],
tool_node="RESEARCHER_TOOLS",
trim_context=True,
)
writer_agent = Agent(
model="gemini-2.5-flash",
provider="google",
system_prompt=[
{
"role": "system",
"content": """You are a writing specialist. Write clear content and save it with
write_document. Then use transfer_to_COORDINATOR.""",
},
],
tool_node="WRITER_TOOLS",
trim_context=True,
)Route each agent to its tools or to the end
Each agent needs one conditional edge. The router sends the graph to the agent’s tool node when the last assistant message has tool calls, back to the agent after a tool result, and to END otherwise. One factory function builds all three routers.
def make_router(agent_node: str, tools_node: str):
"""Build a routing function for one agent and its tool node."""
def route(state: AgentState) -> str:
if not state.context:
return tools_node
last = state.context[-1]
if last.role == "assistant" and getattr(last, "tools_calls", None):
return tools_node
if last.role == "tool":
return agent_node
return END
return routeThe repository file spells out three near-identical functions (should_continue_coordinator and so on). The factory behaves the same.
Assemble and compile the graph
The graph registers all six nodes and adds one conditional edge per agent. The repository example adds no edges out of the tool nodes: handoffs jump via Command. If you adapt it and a regular tool result does not return to its agent, add an explicit edge from the tool node back to the agent.
graph = StateGraph()
graph.add_node("COORDINATOR", coordinator_agent)
graph.add_node("COORDINATOR_TOOLS", coordinator_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)
graph.set_entry_point("COORDINATOR")
for agent_node in ("COORDINATOR", "RESEARCHER", "WRITER"):
tools_node = f"{agent_node}_TOOLS"
graph.add_conditional_edges(
agent_node,
make_router(agent_node, tools_node),
{tools_node: tools_node, END: END},
)
app = graph.compile(checkpointer=checkpointer)Invoke the graph and inspect the messages
The run uses a thread_id for the checkpointer and a recursion_limit so a looping handoff cannot run forever. The loop prints each message and the tools it called, which shows the handoffs.
if __name__ == "__main__":
inp = {
"messages": [
Message.text_message(
"Please research quantum computing and write a brief article about it."
)
]
}
config = {"thread_id": "handoff-demo-001", "recursion_limit": 15}
res = app.invoke(inp, config=config)
for i, msg in enumerate(res["messages"], 1):
print(f"[{i}] {msg.role}: {msg.text()[:200]}")
for tool_call in getattr(msg, "tools_calls", None) or []:
print(" tool called:", tool_call.get("function", {}).get("name", ""))Expected flow
The model decides the exact path, but a typical run looks like this:
- The coordinator explains its choice and calls
transfer_to_RESEARCHER. - The coordinator’s tool node detects the prefix and returns a
CommandtoRESEARCHER. - The researcher calls
search_web, thentransfer_to_WRITER. - The writer calls
write_document, thentransfer_to_COORDINATOR. - The coordinator writes a final summary with no tool calls, so the router returns
END.
If a run stops early or loops, tighten the system prompts. Handoff behavior depends on the model following them.
Try next
- Read Handoff between agents for when to choose handoffs over other patterns.
- Use SwarmAgent to get this wiring generated for you.
- See Agents and tools for how agents and tool nodes interact.
Frequently asked questions
- What is the difference between a handoff and regular routing?
- In regular routing, your graph code decides the next node. With handoff tools, the model decides to transfer to another specialist by calling a transfer_to_<name> tool, and the graph jumps to that node.
- Can I use handoffs with prebuilt agents?
- Yes. SwarmAgent and SupervisorTeamAgent generate handoff tools for you. This example builds the same pattern by hand so you can see each part.
- Do I need explicit edges from tool nodes back to agents?
- No. When a tool node sees a transfer_to_<name> call, it skips running the tool and returns a Command that sends the graph to the node with that name.