Graph-Based Orchestration: Sequential, Parallel, and Handoff Patterns
Master graph-based orchestration patterns for multi-agent systems with LangGraph, including sequential processing, parallel execution, and agent handoff mechanisms.
Published on • September 7, 2026
AI Assistant

Multi-agent systems require careful orchestration. Agents need to coordinate their work, share context, and handle failures gracefully. Graph-based orchestration using LangGraph provides the primitives to build these complex workflows with explicit control over execution flow.
This article covers three fundamental orchestration patterns—sequential, parallel, and handoff—with practical code examples using LangGraph. These patterns form the building blocks for any multi-agent system.
Why This Matters
As AI agents handle more complex tasks, single-agent systems hit limits. One agent can’t be an expert in everything, and forcing it to be leads to poor performance and difficult debugging. Multi-agent systems distribute specialized work across focused agents, but they introduce coordination challenges.
Graph-based orchestration addresses these challenges by making the coordination logic explicit. Instead of relying on implicit context passing or fragile prompt engineering, you define exactly how agents interact through a state graph. This makes multi-agent behavior predictable, debuggable, and testable.
The three patterns covered here—sequential, parallel, and handoff—map to the majority of multi-agent coordination needs. Master these patterns, and you can build sophisticated multi-agent systems for almost any use case.
Sequential Pattern: Chain of Responsibility
The sequential pattern chains agents in a fixed order, where each agent processes the output of the previous one. This is the simplest multi-agent pattern and works well for pipelines where each step transforms or enriches the data.
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
import google.generativeai as genai
genai.configure(api_key="YOUR_API_KEY")
llm = genai.GenerativeModel("gemini-2.5-pro")
# Define specialized agents
def researcher(state: MessagesState):
"""Agent 1: Research the topic."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a research specialist. Given a topic, gather key facts, "
"statistics, and relevant context. Be thorough but concise."
))
response = llm.generate_content(
[system_prompt] + messages
)
return {
"messages": [AIMessage(content=f"RESEARCH:\n{response.text}")]
}
def analyst(state: MessagesState):
"""Agent 2: Analyze the research findings."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a data analyst. Given research findings, identify key trends, "
"patterns, and insights. Structure your analysis clearly."
))
response = llm.generate_content(
[system_prompt] + messages
)
return {
"messages": [AIMessage(content=f"ANALYSIS:\n{response.text}")]
}
def writer(state: MessagesState):
"""Agent 3: Write the final report."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a technical writer. Given research and analysis, write a "
"clear, engaging report. Use markdown formatting."
))
response = llm.generate_content(
[system_prompt] + messages
)
return {
"messages": [AIMessage(content=f"REPORT:\n{response.text}")]
}
# Build the sequential graph
graph = StateGraph(MessagesState)
# Add nodes
graph.add_node("researcher", researcher)
graph.add_node("analyst", analyst)
graph.add_node("writer", writer)
# Define sequential flow
graph.add_edge(START, "researcher")
graph.add_edge("researcher", "analyst")
graph.add_edge("analyst", "writer")
graph.add_edge("writer", END)
# Compile and run
app = graph.compile()
result = app.invoke({
"messages": [HumanMessage(content="Impact of AI on healthcare diagnostics")]
})
print(result["messages"][-1].content)
The sequential pattern is straightforward but limited: if one agent fails or produces poor output, the entire chain suffers. For more complex needs, consider parallel or handoff patterns.
Parallel Pattern: Fan-Out/Fan-In
The parallel pattern runs multiple agents simultaneously, then combines their results. This is useful when agents can work independently on different aspects of a problem.
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
import google.generativeai as genai
from typing import Literal
genai.configure(api_key="YOUR_API_KEY")
llm = genai.GenerativeModel("gemini-2.5-pro")
def technical_analyst(state: MessagesState):
"""Analyze from a technical perspective."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a technical analyst. Analyze the given topic from a "
"technology perspective: capabilities, limitations, technical feasibility."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"TECHNICAL ANALYSIS:\n{response.text}")]
}
def business_analyst(state: MessagesState):
"""Analyze from a business perspective."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a business analyst. Analyze the given topic from a "
"business perspective: market opportunity, ROI, competitive landscape."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"BUSINESS ANALYSIS:\n{response.text}")]
}
def ethics_analyst(state: MessagesState):
"""Analyze from an ethical perspective."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are an ethics analyst. Analyze the given topic from an "
"ethical perspective: fairness, privacy, societal impact, risks."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"ETHICS ANALYSIS:\n{response.text}")]
}
def synthesizer(state: MessagesState):
"""Combine all analyses into a final report."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a senior strategist. Combine the technical, business, and "
"ethics analyses into a comprehensive recommendation report. "
"Highlight agreements and disagreements between perspectives."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"FINAL REPORT:\n{response.text}")]
}
# Build the parallel graph
graph = StateGraph(MessagesState)
# Add all analyst nodes
graph.add_node("technical", technical_analyst)
graph.add_node("business", business_analyst)
graph.add_node("ethics", ethics_analyst)
graph.add_node("synthesizer", synthesizer)
# Fan-out: START connects to all analysts in parallel
graph.add_edge(START, "technical")
graph.add_edge(START, "business")
graph.add_edge(START, "ethics")
# Fan-in: All analysts connect to synthesizer
graph.add_edge("technical", "synthesizer")
graph.add_edge("business", "synthesizer")
graph.add_edge("ethics", "synthesizer")
# End after synthesis
graph.add_edge("synthesizer", END)
# Compile and run
app = graph.compile()
result = app.invoke({
"messages": [HumanMessage(content="Should hospitals adopt AI diagnostic tools?")]
})
# Print the final synthesis
final_message = result["messages"][-1]
print(final_message.content)
The parallel pattern scales well: adding a new analyst perspective requires only adding a node and connecting it to START and the synthesizer. The synthesizer handles combining outputs, which keeps individual analysts decoupled.
Handoff Pattern: Dynamic Agent Delegation
The handoff pattern allows agents to dynamically delegate work to other agents based on the current state. This is the most flexible pattern, enabling complex workflows where the path through the graph isn’t known in advance.
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
import google.generativeai as genai
from typing import Literal
genai.configure(api_key="YOUR_API_KEY")
llm = genai.GenerativeModel("gemini-2.5-pro")
def triage_agent(state: MessagesState):
"""Initial agent that routes to the appropriate specialist."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a triage specialist. Analyze the user's request and determine "
"which specialist should handle it. Respond with one of: "
"TECHNICAL, LEGAL, FINANCE, or GENERAL."
))
response = llm.generate_content([system_prompt] + messages)
# Determine routing based on response
response_text = response.text.upper()
if "TECHNICAL" in response_text:
next_agent = "technical_specialist"
elif "LEGAL" in response_text:
next_agent = "legal_specialist"
elif "FINANCE" in response_text:
next_agent = "finance_specialist"
else:
next_agent = "general_specialist"
return {
"messages": [AIMessage(content=f"Routing to: {next_agent}")],
"next_agent": next_agent
}
def technical_specialist(state: MessagesState):
"""Handle technical questions."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a technical specialist. Provide detailed technical analysis, "
"architecture recommendations, and implementation guidance."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"TECHNICAL RESPONSE:\n{response.text}")]
}
def legal_specialist(state: MessagesState):
"""Handle legal questions."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a legal specialist. Provide legal analysis, compliance guidance, "
"and regulatory considerations. Note: this is informational only."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"LEGAL RESPONSE:\n{response.text}")]
}
def finance_specialist(state: MessagesState):
"""Handle finance questions."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a finance specialist. Provide financial analysis, cost-benefit "
"assessments, and budget recommendations."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"FINANCE RESPONSE:\n{response.text}")]
}
def general_specialist(state: MessagesState):
"""Handle general questions."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"You are a general specialist. Provide a balanced overview and "
"recommend consulting a specialist for detailed analysis."
))
response = llm.generate_content([system_prompt] + messages)
return {
"messages": [AIMessage(content=f"GENERAL RESPONSE:\n{response.text}")]
}
def route_after_triage(state: MessagesState) -> str:
"""Dynamic routing based on triage decision."""
return state.get("next_agent", "general_specialist")
# Build the handoff graph
graph = StateGraph(MessagesState)
# Add all nodes
graph.add_node("triage", triage_agent)
graph.add_node("technical_specialist", technical_specialist)
graph.add_node("legal_specialist", legal_specialist)
graph.add_node("finance_specialist", finance_specialist)
graph.add_node("general_specialist", general_specialist)
# Entry point
graph.add_edge(START, "triage")
# Dynamic routing from triage
graph.add_conditional_edges(
"triage",
route_after_triage,
{
"technical_specialist": "technical_specialist",
"legal_specialist": "legal_specialist",
"finance_specialist": "finance_specialist",
"general_specialist": "general_specialist",
}
)
# All specialists can loop back for follow-up or end
graph.add_edge("technical_specialist", END)
graph.add_edge("legal_specialist", END)
graph.add_edge("finance_specialist", END)
graph.add_edge("general_specialist", END)
# Compile and run
app = graph.compile()
# Test with different queries
result = app.invoke({
"messages": [HumanMessage(content="What's the ROI of implementing RAG?")]
})
print(result["messages"][-1].content)
The handoff pattern adds complexity but enables sophisticated routing logic. The triage agent examines the input and routes to the most appropriate specialist, keeping each specialist focused on its domain.
Combining Patterns
Real-world applications often combine these patterns. Here’s a hybrid example that uses handoff for routing, parallel analysis within specialists, and sequential post-processing:
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
def smart_router(state: MessagesState):
"""Route to parallel analysis or sequential processing based on complexity."""
messages = state["messages"]
system_prompt = SystemMessage(content=(
"Classify this request as COMPLEX (needs multiple analyses) "
"or SIMPLE (single analysis sufficient). "
"Respond with just the classification."
))
# Simplified: in production, use a proper classifier
last_message = messages[-1].content.lower()
is_complex = any(word in last_message for word in
["comprehensive", "full", "complete", "detailed"])
return {"route": "complex" if is_complex else "simple"}
def route_decision(state: MessagesState) -> str:
"""Choose between complex (parallel) and simple (sequential) paths."""
return state.get("route", "simple")
def simple_analysis(state: MessagesState):
"""Quick single-agent analysis."""
# ... single analysis step
return {"messages": [AIMessage(content="Simple analysis result")]}
def parallel_analysis(state: MessagesState):
"""Multi-agent parallel analysis."""
# ... fan-out to multiple analysts
return {"messages": [AIMessage(content="Parallel analysis results")]}
def post_process(state: MessagesState):
"""Common post-processing for both paths."""
# ... format, validate, etc.
return {"messages": [AIMessage(content="Final formatted output")]}
# Build hybrid graph
graph = StateGraph(MessagesState)
graph.add_node("router", smart_router)
graph.add_node("simple", simple_analysis)
graph.add_node("complex", parallel_analysis)
graph.add_node("postprocess", post_process)
graph.add_edge(START, "router")
graph.add_conditional_edges("router", route_decision, {
"simple": "simple",
"complex": "complex"
})
graph.add_edge("simple", "postprocess")
graph.add_edge("complex", "postprocess")
graph.add_edge("postprocess", END)
Best Practices
Start Simple, Add Complexity Gradually
Begin with sequential workflows. Only introduce parallel execution or handoff patterns when you have a concrete reason. Complex graphs are harder to debug and test.
Use Typed State
Define explicit state schemas rather than using untyped dictionaries. Typed state catches errors at definition time and makes your graph’s data flow explicit.
Keep Nodes Focused
Each node should do one thing well. If a node is doing too much, split it into multiple nodes connected by edges. This makes your graph more maintainable and easier to debug.
Test Individual Nodes
Test your nodes in isolation before testing the full graph. Each node is a pure function that takes state and returns state updates—unit test them like any other function.
Trace Everything
Enable LangSmith tracing during development. Visual execution traces make it much easier to understand what’s happening in complex graphs, especially when parallel branches or conditional routing are involved.
Next Steps
Graph-based orchestration is the foundation for production multi-agent systems. The sequential, parallel, and handoff patterns covered here give you the building blocks for almost any coordination need.
Start by mapping your agent coordination needs to these patterns. Most complex workflows are compositions of these three primitives. Once you’re comfortable with the basics, explore advanced features like human-in-the-loop interrupts, persistent state across sessions, and dynamic graph construction.
The key insight is that graph-based orchestration makes multi-agent coordination explicit and debuggable. Instead of hoping your agents coordinate correctly, you define exactly how they should interact—and you can trace and verify that behavior in production.