Objective
What you're building
A trivial 2-node LangGraph graph — no research logic yet. The point is seeing the mechanics (nodes, edges, state) work on something simple enough to fully understand before Lesson 2 makes the state schema real.
Chains run in order. graphs don't have to.
A prompt chain — do step A, feed its output into step B, feed that into step C — is fundamentally sequential, even if you dress it up as multiple LLM calls. If steps B and C don't actually depend on each other's output (this course's Web Researcher, Technical Analyst, and Market Analyst all research the SAME question independently), running them sequentially wastes exactly the time Course 03's asyncio.gather lesson taught you not to waste.
A graph is a chain that can also fan out and fan back in
LangGraph models your system as nodes (units of work — here, each is one agent) connected by edges (data flow). Three nodes with no edges between each other, all fed by the same upstream node and all feeding the same downstream node, run in parallel automatically — that's the "fan-out, fan-in" pattern this course's 3 specialists use, and it's the actual reason to reach for a graph library instead of hand-rolling asyncio.gather calls (which would also work, but you'd be reimplementing state management, conditional routing, and checkpointing yourself).
The trivial graph
from typing_extensions import TypedDict
from langgraph.graph import START, END, StateGraph
class State(TypedDict):
question: str
answer: str
def answer_node(state: State) -> dict:
return {"answer": f"You asked: {state['question']}"}
builder = StateGraph(State)
builder.add_node("answer", answer_node)
builder.add_edge(START, "answer")
builder.add_edge("answer", END)
graph = builder.compile()
result = graph.invoke({"question": "What is LangGraph?", "answer": ""})
print(result)
Every node is a plain function that takes the current state and returns a PARTIAL update (a dict of just the fields it's changing, not the whole state) -- LangGraph merges that into the shared state object automatically. START and END are LangGraph's built-in markers for graph entry and exit. This is the entire mental model; the research team you build over the next 5 lessons is this same pattern with 5 real agent nodes and a genuinely useful state schema instead of one toy field.
A version note, since this library moves fast
LangGraph is now well past its early 0.x releases (1.2.x as of mid-2026) -- if you find a tutorial using graph.add_conditional_edges with a very different signature than what you see in the current docs, or one still using the pre-1.0 MessageGraph class, treat it as describing an older version. The node/edge/state model in this lesson has been stable across that transition; the exact helper APIs around it have not always been.
You're designing the research team graph. The Web Researcher, Technical Analyst, and Market Analyst all take the SAME sub-tasks from the Planner and don't depend on each other's output. Should they be modeled as 3 sequential edges (Planner -> Web -> Technical -> Market -> Synthesizer) or as 3 parallel edges from Planner, all feeding into Synthesizer?
This is exactly the "chains vs. graphs" distinction from earlier in this lesson: when steps are truly independent, modeling them as parallel edges from a shared upstream node (Planner) into a shared downstream node (Synthesizer) captures that independence and lets LangGraph actually run them concurrently -- sequential edges would work correctly but pay a real latency cost for no reason, since research question answers for one specialist have no bearing on what another specialist should look up.
What you're building today
hello_graph.py: a 2-node graph exactly like the one above, run and inspected — proving you understand state merging and the START/END/add_node/add_edge mechanics before Lesson 2 makes the state schema genuinely complex.