Objective
The market analyst: same mechanics, market-focused prompt
Reuses Lesson 4's _run_specialist helper with a focus instruction on trends, adoption data, and competitive landscape — no new mechanics, just the third application of a pattern you've now built twice.
Wiring real fan-out
builder = StateGraph(ResearchState)
builder.add_node("planner", planner_node)
builder.add_node("web_researcher", web_researcher_node)
builder.add_node("technical_analyst", technical_analyst_node)
builder.add_node("market_analyst", market_analyst_node)
builder.add_node("synthesizer", synthesizer_node)
builder.add_edge(START, "planner")
# 3 edges FROM THE SAME NODE = fan-out. LangGraph runs all 3 concurrently.
builder.add_edge("planner", "web_researcher")
builder.add_edge("planner", "technical_analyst")
builder.add_edge("planner", "market_analyst")
# 3 edges TO THE SAME NODE = fan-in. Synthesizer waits for all 3.
builder.add_edge("web_researcher", "synthesizer")
builder.add_edge("technical_analyst", "synthesizer")
builder.add_edge("market_analyst", "synthesizer")
builder.add_edge("synthesizer", END)
graph = builder.compile()
The fan-out/fan-in pattern here is entirely in the EDGES, not in any special "parallel" flag or function -- 3 edges leaving planner tells LangGraph these 3 nodes have no dependency on each other and can run concurrently; 3 edges arriving at synthesizer tells it to wait until all 3 have completed before running. This is the exact mechanism Lesson 1's checkpoint anticipated.
Verify it's actually parallel — don't just assume
"I used LangGraph" doesn't guarantee real concurrency -- prove it
Add timing logs around each specialist node and run the graph. If genuinely parallel, total wall-clock time should be roughly the SLOWEST specialist's individual time, not the sum of all 3. If you see something closer to the sum, check whether your node functions are truly independent (a shared mutable object accidentally serializing them) before assuming the graph structure alone is the whole story.
Rate limiting: 3 simultaneous calls hit the same API limit
Real concurrency creates a real rate-limit risk
Running 3 specialists concurrently means 3 simultaneous calls to the same Anthropic API key -- fine at small scale, but a genuine risk once you're testing repeatedly or running longer research sessions. An asyncio.Semaphore(2) around the actual API call (not around the whole node) caps concurrent in-flight requests without serializing the rest of each node's work (search, extraction) that doesn't touch the rate-limited resource.
You add timing logs and find the graph takes almost exactly as long as running all 3 specialists sequentially would. What's the most likely cause, given the edges are wired correctly for fan-out?
Correct edge wiring is necessary but not sufficient -- if the underlying node functions are synchronous and the graph isn't invoked in a way that actually schedules them concurrently (e.g. using the graph's async invocation with genuinely async or thread-offloaded node functions), you can have "parallel" edges that still execute one after another in practice. This is worth verifying with real timing, exactly as this lesson's callout above says, rather than trusting the edge diagram alone.
What you're building
research_graph.py: the Market Analyst node, the full graph with fan-out from Planner to all 3 specialists and fan-in to a (stub, for now) Synthesizer, wrapped with an asyncio.Semaphore(2) around the actual API calls — run against a real question with timing logs proving wall-clock time is close to the slowest specialist, not the sum of all 3.