Deep Dive into LangGraph Swarm: How Agents Transfer Control with the Handoff Mechanism
This article explains the Swarm collaboration model in LangGraph, contrasting it with Supervisor, detailing the handoff tool that atomically updates the active_agent state and routes control, and provides a complete travel‑booking example, custom handoff creation, common pitfalls, and best‑practice tips.
01 Swarm Essence: A Relay Race, Not a Dispatch
Swarm is inspired by bee colonies: there is no central controller, each Agent decides "who should continue the task" based on local information. Compared with Supervisor, control resides in the currently active Agent, routing is decided by the Agent’s handoff tool, context is shared via State, token usage is lower because agents hand off directly.
02 Handoff Mechanism: Tool Call Triggers Control Transfer
The handoff tool simply returns a Command object. Command updates the active_agent field in the shared State and specifies a goto target, performing state update and node jump as a single atomic operation.
import { createReactAgent } from "@langchain/langgraph/prebuilt");
import { createHandoffTool } from "@langchain/langgraph-swarm");
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o" });
// description must state trigger condition + action verb
const transferToHotelAgent = createHandoffTool({
agentName: "hotel_agent",
description: "When the user needs to query, book, or cancel a hotel, invoke this tool to hand off to the hotel expert"
});
const flightAgent = createReactAgent({
llm: model,
tools: [searchFlights, transferToHotelAgent],
name: "flight_agent",
prompt: "You are a flight‑booking expert. After completing the flight task, if the user also needs a hotel, hand off to hotel_agent."
});03 SwarmState: Why Context Is Not Lost
SwarmState extends MessagesAnnotation with an active_agent field whose reducer simply replaces the previous value. When a handoff occurs, only this field changes, while the full messages array remains unchanged, so the next Agent receives the complete conversation history.
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
const SwarmState = Annotation.Root({
...MessagesAnnotation.spec,
active_agent: Annotation<string>({
reducer: (_, update) => update,
default: () => ""
})
});
// Before handoff
{ messages: [full history], active_agent: "flight_agent" }
// After handoff
{ messages: [full history], active_agent: "hotel_agent" }04 Full Demo: Travel‑Booking Swarm
A runnable Swarm combines a flight Agent and a hotel Agent. Tools for searching flights and hotels are defined with tool, and handoff tools toHotelAgent and toFlightAgent are created. The Swarm is compiled with createSwarm, a fixed thread_id ensures state continuity, and the result shows a successful flight lookup and hotel recommendation.
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { createSwarm, createHandoffTool } from "@langchain/langgraph-swarm";
import { MemorySaver } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
// business tools omitted for brevity
const AGENTS = { FLIGHT: "flight_agent", HOTEL: "hotel_agent" } as const;
const toHotelAgent = createHandoffTool({ agentName: AGENTS.HOTEL, description: "When the user needs to book a hotel, call this" });
const toFlightAgent = createHandoffTool({ agentName: AGENTS.FLIGHT, description: "When the user needs to book a flight, call this" });
const flightAgent = createReactAgent({ llm: model, tools: [searchFlights, toHotelAgent], name: AGENTS.FLIGHT, prompt: "You are a flight‑booking expert. Complete the flight task before handing off." });
const hotelAgent = createReactAgent({ llm: model, tools: [searchHotels, toFlightAgent], name: AGENTS.HOTEL, prompt: "You are a hotel‑booking expert. Complete the hotel task before handing off." });
const swarm = createSwarm({ agents: [flightAgent, hotelAgent], defaultActiveAgent: AGENTS.FLIGHT })
.compile({ checkpointer: new MemorySaver() });
const result = await swarm.invoke(
{ messages: [{ role: "user", content: "Book a Beijing‑Shanghai flight and a hotel near the Bund" }] },
{ configurable: { thread_id: "user-123-trip-booking" } }
);
// Output: Flight CA1234 found, Bund Peace Hotel recommended05 active_agent Router: Traffic Controller of the Graph
The router reads active_agent. If empty, it routes to defaultActiveAgent; otherwise it routes to the Agent indicated by active_agent. This yields three cases: first round (no active_agent), subsequent rounds (reuse previous active_agent), and after handoff (active_agent updated to target).
06 Custom Handoff: Full Control Over Graph Structure
When create_swarm is not suitable, a handoff tool can be written manually. The tool returns a Command with update.active_agent and a goto target, and must include a ToolMessage that closes the original tool_call by providing the correct tool_call_id. Manual graph construction allows insertion of approval nodes, jump restrictions, or human‑in‑the‑loop steps.
function makeHandoffTool(targetAgent: string, description: string) {
return tool(async (_, config) => new Command({
update: { active_agent: targetAgent, messages: [new ToolMessage({ content: `Handing off to ${targetAgent}`, tool_call_id: config.toolCall!.id })] },
goto: targetAgent,
}), { name: `transfer_to_${targetAgent}`, description, schema: z.object({}) });
}
const builder = new StateGraph(MyState)
.addNode("triage_agent", triageAgent)
.addNode("billing_agent", billingAgent)
.addNode("tech_agent", techAgent)
.addConditionalEdges("__start__", s => s.active_agent, { triage_agent: "triage_agent", billing_agent: "billing_agent", tech_agent: "tech_agent" })
.compile({ checkpointer: new MemorySaver() });07 Common Pitfalls: Five Issues That Break Swarm
Handoff tool description too vague – LLM cannot decide when to hand off. Include explicit trigger and action verb.
Misspelled defaultActiveAgent – runtime error; use an enum like AGENTS to avoid hard‑coded strings.
Inconsistent thread_id – each invoke must use the same thread_id to keep the checkpoint.
Infinite handoff loops – ensure prompts require task completion before handoff and set recursionLimit (e.g., 10) to cap API usage.
Forgot to close tool_call in manual handoff – ToolMessage.tool_call_id must match the LLM’s call ID; otherwise the message chain is broken.
Conclusion
The Swarm’s “baton” is the active_agent field; the handoff tool changes it, effecting an atomic state update and node transition. SwarmState keeps the full message history, so context is never lost. create_swarm handles routing and edge registration automatically, while a hand‑written graph offers complete control. Paying attention to handoff description, consistent thread_id, and proper tool_call_id eliminates most bugs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
James' Growth Diary
I am James, focusing on AI Agent learning and growth. I continuously update two series: “AI Agent Mastery Path,” which systematically outlines core theories and practices of agents, and “Claude Code Design Philosophy,” which deeply analyzes the design thinking behind top AI tools. Helping you build a solid foundation in the AI era.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
