Agent Orchestration with LangGraph
Multi-agent orchestration and durable execution design built on the LangGraph StateGraph
Contents
Turn the execution flow, state, and failure recovery of a multi-agent system into a LangGraph StateGraph.
Why orchestration is needed
In a multi-agent system, communication between agents, state management, and control of the execution flow matter as much as the reasoning ability of any individual agent. Wiring nine agents into a plain sequential for-loop hits a set of hard limits.
| Limit | Description | Consequence |
|---|---|---|
| No failure recovery | An error at step 7 forces a rerun from the beginning | Wasted API calls and cost |
| No parallel execution | Independent analysis steps still wait in line | Higher total latency |
| No conditional branching | The whole pipeline runs even when data quality is bad | Decisions made on corrupted data |
| No state tracking | Nothing records how far the run got | Debugging and auditing are impossible |
| No human intervention | High-risk decisions have no approval step | Automation risk is left exposed |
LangGraph addresses all of these with a graph-based state machine (StateGraph).
Core LangGraph concepts
StateGraph: a graph-based workflow
LangGraph models an agent workflow as a stateful, controlled execution graph. A node is one agent's unit of execution, and an edge defines a state transition. The graph is laid out as a directed acyclic graph (DAG).
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
class PipelineState(TypedDict):
project_id: str
market: str
config: dict
scan_results: list[ScanResult]
news_items: list[NewsItem]
position_alerts: list[PositionAlert]
signals: list[Signal]
debate_result: dict | None
final_decision: dict | None
errors: list[str]
graph = StateGraph(PipelineState)
PipelineStateis the shared state for the whole workflow. Each node reads and updates it, and the checkpointer saves a snapshot after every step.
Core building blocks
| Building block | Role |
|---|---|
| Node | An agent's unit of execution (a function) |
| Edge | A state transition between nodes |
| Conditional Edge | A branch chosen by a condition |
| State | Shared state (a TypedDict) |
| Checkpointer | Saves state when each node completes (like a save point in a game) |
| Thread | Identifier for one execution instance |
Designing the StateGraph DAG: a trading pipeline
Mapping onto the three Sense-Think-Act layers
The nine agents split across the three Sense-Think-Act layers, and the LangGraph StateGraph wires them into a DAG.
Graph definition
graph = StateGraph(PipelineState)
# --- Sense Layer ---
graph.add_node("data_quality_gate", data_quality_gate)
graph.add_node("volume_scanner", volume_scanner)
graph.add_node("news_collector", news_collector)
graph.add_node("position_sensor", position_sensor)
# --- Think Layer (fan-out / fan-in) ---
graph.add_node("quant_analyst", quant_analyst)
graph.add_node("llm_analyst", llm_analyst)
graph.add_node("sector_rotation", sector_rotation)
graph.add_node("analysis_merge", merge_analysis_results)
graph.add_node("bull_bear_debate", bull_bear_debate)
graph.add_node("ensemble_voter", ensemble_voter)
# --- Act Layer ---
graph.add_node("risk_sentinel", risk_sentinel)
graph.add_node("trade_executor", trade_executor)
# Edge definitions
graph.set_entry_point("data_quality_gate")
# Data Quality Gate: conditional branch
graph.add_conditional_edges(
"data_quality_gate",
lambda s: "continue" if not s["errors"] else "end",
{"continue": "volume_scanner", "end": END},
)
# Sense Layer runs sequentially
graph.add_edge("volume_scanner", "news_collector")
graph.add_edge("news_collector", "position_sensor")
# Fan-out: parallel analysis
graph.add_edge("position_sensor", "quant_analyst")
graph.add_edge("position_sensor", "llm_analyst")
graph.add_edge("position_sensor", "sector_rotation")
# Fan-in: results converge
graph.add_edge("quant_analyst", "analysis_merge")
graph.add_edge("llm_analyst", "analysis_merge")
graph.add_edge("sector_rotation", "analysis_merge")
# Second half of the Think Layer
graph.add_edge("analysis_merge", "bull_bear_debate")
graph.add_edge("bull_bear_debate", "ensemble_voter")
graph.add_edge("ensemble_voter", "risk_sentinel")
# Risk Sentinel: conditional branch
graph.add_conditional_edges(
"risk_sentinel",
lambda s: "execute" if s["final_decision"].get("approved") else "end",
{"execute": "trade_executor", "end": END},
)
graph.add_edge("trade_executor", END)Fan-out / fan-in parallelism
What parallelism buys
Inside the decision loop, Quant Analyst (p95 < 300ms), Sector Rotation (p95 < 300ms), and LLM Analyst (p95 < 8s) are independent of one another. Here p95 means the 95th percentile latency: 95% of requests finish within that time. Run sequentially they take at least 8.6 seconds; run in parallel the total converges on the slowest one, roughly 8 seconds.
How fan-out works in LangGraph
When one node has edges to several nodes, LangGraph fans out automatically and runs them in parallel. Fan-in happens when several nodes point at the same next node; that node waits until every predecessor has finished.
# position_sensor -> [quant, llm, sector] : fan-out
graph.add_edge("position_sensor", "quant_analyst")
graph.add_edge("position_sensor", "llm_analyst")
graph.add_edge("position_sensor", "sector_rotation")
# [quant, llm, sector] -> analysis_merge : fan-in
graph.add_edge("quant_analyst", "analysis_merge")
graph.add_edge("llm_analyst", "analysis_merge")
graph.add_edge("sector_rotation", "analysis_merge")The analysis_merge node combines the three agents' outputs into one report. Because each agent's result accumulates in the shared state, the join node can read all of them.
Checkpointing and durable execution
How the checkpointer behaves
The LangGraph checkpointer saves a snapshot of the entire state after every super-step, meaning after each node completes. That enables the following.
| Capability | Description |
|---|---|
| Fault tolerance | Resume from the last checkpoint after a failure |
| Human-in-the-loop | Pause at a given step and wait for human approval |
| Time travel | Roll back to an earlier state and simulate a different decision |
| Audit trail | Per-step state is recorded, so decisions can be traced |
A PostgreSQL-backed checkpointer
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string(DATABASE_URL)
app = graph.compile(checkpointer=checkpointer)
# Run
result = await app.ainvoke(
initial_state,
config={"configurable": {"thread_id": "pipeline-run-uuid"}}
)thread_id is the primary key for checkpoint lookups. Rerunning with the same thread_id resumes automatically from the step after the last checkpoint.
Restart sequence
Durable execution principles
Durable execution is a core LangGraph pattern, and it follows these principles.
| Principle | Trading application |
|---|---|
| Deterministic | Up to Risk Sentinel, the same input yields the same output |
| Idempotent | Order submission is deduplicated by idempotency_key |
| Side effects isolated | Order submission, an external API call, is wrapped as a task |
In trading, exactly-once execution is hard to guarantee perfectly because of the broker and exchange boundary. So each order carries an
idempotency_key, and on restart an idempotency check verifies whether an order with that key was already submitted.
Human-in-the-loop
When a human is needed
In automated trading, some situations call for human confirmation rather than a purely mechanical judgment.
- Additional trades when the daily loss limit is close
- Sudden regime shifts such as a VIX spike or a black swan event
- Large buys made while news reliability is very low
- The first live trade right after a new strategy or prompt change
Implementing human-in-the-loop in LangGraph
The Risk Sentinel output carries a requires_human_approval flag, and the LangGraph interrupt() function pauses execution.
from langgraph.types import interrupt
def risk_sentinel(state: PipelineState) -> PipelineState:
intent = state["final_decision"]
portfolio = get_portfolio_snapshot(state["project_id"])
decision = evaluate_risk(intent, portfolio)
if decision.requires_human_approval:
# interrupt(): pause and wait for human approval. The return value is what the human sends on resume.
approved = interrupt(
f"High-risk trade detected: {decision.reasons}"
)
state["final_decision"]["approved"] = approved
return state
state["final_decision"]["approved"] = decision.approved
return stateA node that calls interrupt() stops at that point, and when the user resumes with Command(resume=value) the node runs again from its start. The older approach of raising NodeInterrupt was replaced by the interrupt() function in LangGraph v0.4. Thanks to checkpoints, none of the steps before the pause have to run again.
Branching with conditional edges
Data quality gate
The pipeline entry point validates data quality and aborts the whole run when the data falls below the bar.
graph.add_conditional_edges(
"data_quality_gate",
lambda s: "continue" if not s["errors"] else "end",
{"continue": "volume_scanner", "end": END},
)The data quality gate checklist:
| Check | Threshold | On failure |
|---|---|---|
| API response time | p95 < 3s | Abort after 3 retries |
| Quote freshness | now - data timestamp < 5 min | Warn, abort past 10 min |
| Price outliers | Within 30% of the previous close | Check for splits and rights offerings |
| Missing required symbols | At least 90% of the universe fetched | Abort below the threshold |
| News API availability | At least one source responds | Proceed without news if all fail |
Risk Sentinel branch
The risk verdict routes the run to execute or to end.
graph.add_conditional_edges(
"risk_sentinel",
lambda s: "execute" if s["final_decision"].get("approved") else "end",
{"execute": "trade_executor", "end": END},
)The Risk Sentinel verdict has five possible values.
| Verdict | Meaning | Follow-up |
|---|---|---|
| APPROVE | Approved as proposed | Passed to Trade Executor |
| RESIZE | Approved after size adjustment | Executed with the adjusted intent |
| HOLD | Conditionally deferred | Re-evaluated once the condition holds |
| REJECT | Rejected | Logged, then the run ends |
| KILL | Emergency stop | Cancel every open order |
Event-sourced state persistence
A three-tier storage layout
State persistence in the trading system uses three tiers.
| Store | Nature | Role |
|---|---|---|
| Kafka | Immutable, append-only | Records every event, supports replay and audit |
| Redis | Mutable, TTL-based | Real-time cache, pub/sub, rate limiting, agent weights |
| PostgreSQL | Durable | LangGraph checkpoints, trade history, reflection memos |
Event envelope schema
Every message between agents uses a common envelope.
{
"event_id": "uuid-v7",
"ts": "2026-02-21T09:15:00.123+09:00",
"event_type": "QUANT_DONE",
"correlation_id": "pipeline-run-uuid",
"project_id": "project-uuid",
"agent": "quant_analyst",
"payload": {
"symbol": "005930",
"signal": 0.72,
"confidence": 0.68,
"feature_vector": { "rsi_14": 32.5, "macd_signal": 0.003 }
},
"schema_version": "v2",
"idempotency_key": "uuid-for-dedup"
}idempotency_key and correlation_id carry the weight here. The first prevents duplicate processing; the second ties together every event belonging to a single pipeline run.
LangGraph compared with AutoGen
Framework characteristics
| Dimension | LangGraph | AutoGen |
|---|---|---|
| Modeling approach | Graph / state machine | Event-driven conversation |
| Execution flow | Declared explicitly as a DAG | Messaging between agents |
| State management | Shared TypedDict state | Conversation history |
| Checkpointing | Built in (PostgreSQL, SQLite) | Must be implemented externally |
| Durable execution | Native support | Not supported |
| Human-in-the-loop | Built-in interrupt | UserProxyAgent |
| Parallel execution | Built-in fan-out / fan-in | Teams / GroupChat |
| Debugging | Time travel (state rewind) | Conversation logs |
The TradingAgents framework
TradingAgents proposes a multi-agent structure modeled on a real trading organization. Seven roles (fundamental analyst, sentiment analyst, news analyst, technical analyst, bull/bear researcher, trader, risk manager) collaborate across a five-stage pipeline.
Analyst Team -> Researcher Team -> Trader -> Risk Team -> Fund Manager
(4 in parallel) (bull/bear debate) (synthesis) (3-view debate) (final approval)In a Q1 2024 backtest it reported cumulative returns of 26.62% on AAPL (Sharpe 8.21), 24.36% on GOOGL (Sharpe 6.39), and 23.21% on AMZN (Sharpe 5.60). These are the numbers reported in the paper (arxiv:2412.20138), drawn from a narrow window of three symbols over three months. A Sharpe near 8 is hard to reproduce in live trading, so treat live reproduction as an open question.
Choosing a framework
LangGraph fits a trading system better. The reasons are that (1) checkpointing makes failure recovery possible, (2) conditional edges model risk gates naturally, (3) human-in-the-loop is built in, and (4) time travel makes it possible to simulate what a different decision would have produced.
AutoGen is strong at free-form conversation between agents and dynamic team composition, so its GroupChat pattern can suit a debate structure such as bull/bear. Using both frameworks together is also viable.
Per-agent SLOs and failure modes
Each agent gets a service level objective (SLO) expressed as a latency target. In the table, p99 means the 99th percentile latency: 99% of requests finish within that time.
Latency and throughput targets
| Agent | Suggested latency SLO | Concurrency | Main failure modes | Mitigation |
|---|---|---|---|---|
| Volume Scanner | p99 < 200ms | Hundreds of symbols per run | Delayed or missing data | Watermarks, cooldown |
| News Collector | p95 < 3s | Tens of articles per run | Duplicates, rumors | Dedup, source reliability scores |
| Position Sensor | p99 < 200ms | All symbols per project | Stale quotes | Redis cache, DB reconciliation |
| Quant Analyst | p95 < 300ms | Once per ticket | Feature errors | Pinned feature versions |
| LLM Analyst | p95 < 8s | 10 concurrent | Hallucination, overconfidence | Cited evidence, critic loop |
| Sector Rotation | p95 < 300ms | Periodic | Regime misreads | Hysteresis |
| Bull/Bear Debate | p95 < 10s | A few concurrent | Failure to converge | Round cap of 2 |
| Ensemble Voter | p95 < 200ms | Once per ticket | Overfitting | Calibration |
| Risk Sentinel | p99 < 50ms | Required before every order | Limits not applied | Hard rules take precedence |
| Trade Executor | p99 < 100ms | Orders per second | Rejected or unfilled orders | Requote, timeout |
Per-agent I/O flow
IS in the Trade Executor output is implementation shortfall, the gap between the target price and the actual fill price, expressed in basis points (1bps = 0.01%).
Operations: testing, canary rollout, kill switch
A three-stage test regime
| Stage | Description | Regulatory basis |
|---|---|---|
| Regression tests | Verify existing behavior on every code change | MiFID II RTS 6: conformance testing |
| Simulation | Backtesting plus paper trading | SEC Rule 15c3-5 |
| Canary rollout | Start at 1-5% of capital, then expand | Google SRE canary releases |
Canary rollout principles
In trading the canary unit is capital and exposure rather than traffic.
- New strategy or model change: start at 1-5% of total capital, then monitor performance, risk, and slippage
- LLM prompt or debate protocol change: apply first to a limited sector or a handful of symbols, then check for drift
- Keep pinned versions and event-log-based replay ready so any problem can be rolled back to the previous version immediately
Kill switch
The kill switch is an emergency control that cancels every open order and halts the pipeline.
Every LangGraph node checks the Redis kill switch flag before it runs. If the flag is set, the node exits immediately.
Infrastructure stack summary
| Technology | Role | Primary use |
|---|---|---|
| LangGraph | Orchestration | StateGraph DAG, checkpoints, conditional branching, human-in-the-loop |
| Kafka | Event stream | Pipeline, trade, and position event topics; audit log |
| Redis | Real-time cache | Quote cache, pub/sub, rate limiter, kill switch, agent weights |
| Celery | Scheduling and async work | Beat (periodic triggers), position sensor polling, reflection, reports |
| PostgreSQL | Durable storage | Checkpoints, trade history, reflection memos, weight snapshots |
LangGraph answers the question of how agents should be wired together and executed. It bundles a graph-based DAG, checkpointing, conditional branching, parallel execution, and human-in-the-loop into one framework, so a production-grade multi-agent system can be built without hand-extending a sequential pipeline.
Summary
A sequential for-loop leaves failure recovery, parallel execution, conditional branching, state tracking, and human intervention all as things you must build yourself. LangGraph folds them into a single StateGraph: nodes and edges declare the execution flow, and a TypedDict holds the shared state. The checkpointer saves state after every node, so a failed run resumes from its last point, and interrupt() collects human approval before a high-risk decision. Conditional edges model the data quality gate and the risk gate naturally.
LangGraph fits domains such as trading, where checkpointing, conditional branching, and human-in-the-loop are all required. AutoGen is worth pairing in when free-form conversational debate is the central need.
References
- LangGraph Persistence: https://docs.langchain.com/oss/python/langgraph/persistence
- LangGraph Durable Execution: https://docs.langchain.com/oss/python/langgraph/durable-execution
- TradingAgents: Multi-Agents LLM Financial Trading Framework (arxiv:2412.20138)
- AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation (arxiv:2308.08155)
- QuantAgents: Towards Multi-agent Financial System via Simulated Trading (arxiv:2510.04643)
- Event Sourcing Pattern: https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing
- SEC Rule 15c3-5: Risk Management Controls for Brokers
- MiFID II RTS 6: Commission Delegated Regulation (EU) 2017/589