| name | langchain-handoffs |
| description | Implement LangChain handoffs — state-driven behavior changes between agents or configurations using tool-based transitions and Command. Use when building multi-stage conversational flows, customer support pipelines, sequential workflows where capabilities unlock after preconditions, or when an agent needs to transfer control to another agent. |
LangChain Handoffs
State variable (e.g. current_step, active_agent) persists across turns. Tools update it via Command; middleware or routing logic reads it to change behavior.
Pattern Selection
| Approach | Use when |
|---|
| Single agent + middleware | One agent, dynamic prompt/tools per step. Simpler — prefer this. |
| Multiple agent subgraphs | Agents need bespoke graph implementations (reflection, retrieval loops). Requires careful context engineering. |
Single Agent with Middleware
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.tools import tool, ToolRuntime
from langchain.messages import ToolMessage
from langgraph.types import Command
from langgraph.checkpoint.memory import MemorySaver
from typing import Callable
class SupportState(AgentState):
current_step: str = "triage"
warranty_status: str | None = None
@tool
def record_warranty_status(status: str, runtime: ToolRuntime[None, SupportState]) -> Command:
"""Record warranty status and move to specialist step."""
return Command(update={
"messages": [ToolMessage(content=f"Status: {status}", tool_call_id=runtime.tool_call_id)],
"warranty_status": status,
"current_step": "specialist",
})
@wrap_model_call
def apply_step_config(request: ModelRequest, handler: Callable) -> ModelResponse:
step = request.state.get("current_step", "triage")
configs = {
"triage": {"prompt": "Collect warranty info.", "tools": [record_warranty_status]},
"specialist": {"prompt": "Resolve issue. Warranty: {warranty_status}", "tools": [provide_solution]},
}
cfg = configs[step]
return handler(request.override(
system_prompt=cfg["prompt"].format(**request.state),
tools=cfg["tools"],
))
agent = create_agent(
model, tools=[record_warranty_status, provide_solution],
state_schema=SupportState,
middleware=[apply_step_config],
checkpointer=MemorySaver(),
)
Multiple Agent Subgraphs
Handoff tool — always pass AIMessage + ToolMessage pair
from langchain.messages import AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langgraph.types import Command
@tool
def transfer_to_sales(runtime: ToolRuntime) -> Command:
"""Transfer to the sales agent."""
last_ai_msg = next(m for m in reversed(runtime.state["messages"]) if isinstance(m, AIMessage))
return Command(
goto="sales_agent",
update={
"active_agent": "sales_agent",
"messages": [last_ai_msg, ToolMessage(content="Transferred", tool_call_id=runtime.tool_call_id)],
},
graph=Command.PARENT,
)
Why the pair? LLMs expect every tool call to have a ToolMessage response. Without the pair the receiving agent sees malformed history. Pass only these two messages — not the full subagent history — to avoid context bloat.
Graph wiring
from typing import Literal
from langchain.agents import AgentState, create_agent
from langchain.messages import AIMessage
from langgraph.graph import StateGraph, START, END
from typing_extensions import NotRequired
class MultiAgentState(AgentState):
active_agent: NotRequired[str]
sales_agent = create_agent(model, tools=[transfer_to_support], system_prompt="...")
support_agent = create_agent(model, tools=[transfer_to_sales], system_prompt="...")
def route_after_agent(state: MultiAgentState) -> Literal["sales_agent", "support_agent", "__end__"]:
last = state["messages"][-1]
if isinstance(last, AIMessage) and not last.tool_calls:
return "__end__"
return state.get("active_agent", "sales_agent")
builder = StateGraph(MultiAgentState)
builder.add_node("sales_agent", lambda s: sales_agent.invoke(s))
builder.add_node("support_agent", lambda s: support_agent.invoke(s))
builder.add_conditional_edges(START, lambda s: s.get("active_agent") or "sales_agent", ["sales_agent", "support_agent"])
builder.add_conditional_edges("sales_agent", route_after_agent, ["sales_agent", "support_agent", END])
builder.add_conditional_edges("support_agent", route_after_agent, ["sales_agent", "support_agent", END])
graph = builder.compile()
Key Rules
- Always include
ToolMessage when a tool updates messages — missing it breaks conversation history.
Command.PARENT in subgraph tools — routes the parent graph, not the subagent's own graph.
- Prefer single agent + middleware — simpler, no context engineering risk.
checkpointer required — state must persist across turns for transitions to work.
- Pass only the pair during subgraph handoffs — not full subagent message history.
- End condition: last message is
AIMessage with no tool_calls → return "__end__".
Related Skills
- Context engineering:
langchain-context-engineering
- Subgraphs & persistence:
langgraph-subgraphs
- Multi-agent patterns:
langchain-multi-agent
- HITL / interrupts:
langgraph-human-in-the-loop