We’re building a small three-agent content crew in LangGraph: a researcher gathers facts, a writer drafts, and an editor decides whether the draft is good enough or should loop back for revision. The stack is intentionally simple: LangGraph for orchestration and langchain-anthropic for Claude. You’ll use a shared TypedDict state, StateGraph, add_node, add_edge, a conditional_edge-style routing step via add_conditional_edges, and a MemorySaver checkpoint so the graph can persist thread state between invocations. If you want background first, see our LangGraph guide. If you’re comparing orchestration styles, our CrewAI walkthrough is the useful contrast.
- What this LangGraph multi-agent tutorial covers’re building and what you need
- Stage 1 of the LangGraph multi-agent build: install packages and set your API key
- Stage 2: Define the LangGraph multi-agent shared state with TypedDict
- Stage 3: Create the Claude model and the three agents
- Stage 4: Build the LangGraph multi-agent graph with add_node, add_edge, and conditional routing
- Stage 5: Add LangGraph multi-agent checkpointing with MemorySaver
- Stage 6: Run the LangGraph multi-agent workflow end to end
- Stage 7: Full working example in one file
- Stage 8: What to watch for when adapting the LangGraph multi-agent pattern
- Frequently asked questions
- What makes LangGraph a good fit for multi-agent systems?
- Do I need a separate model for each agent in this tutorial?
- Why use MemorySaver if it is not durable?
- Primary sources
What this LangGraph multi-agent tutorial covers’re building and what you need
3
agents in the crew
Researcher, writer, editor
~120
lines of Python
Core example without extras
1
model integration
Claude via langchain-anthropic
The goal is a minimal but real LangGraph multi-agent tutorial: one graph, three role-specific agents, and a revision loop. The researcher produces source-backed notes from a topic. The writer turns those notes into a draft. The editor checks the draft against a quality bar and either approves it or sends it back for another pass. This pattern is small enough to understand quickly, but it already demonstrates why graph orchestration matters: you get explicit state, deterministic routing, and resumability.
Prerequisites are straightforward. You need Python 3.10+ and an Anthropic API key. The LangChain docs for Anthropic cover the package and environment variable setup at python.langchain.com/docs/integrations/chat/anthropic/. For LangGraph concepts and API references, the main docs live at langchain-ai.github.io/langgraph.
This tutorial keeps tools out of the first version so the orchestration stays clear. The researcher will still be useful because we constrain it to produce structured notes and explicit caveats. Later, you can attach web search or internal retrieval. The important part here is the graph shape, not the breadth of external integrations.
📌 Prereqs. You need an Anthropic API key in ANTHROPIC_API_KEY, Python 3.10+, and the packages langgraph, langchain, and langchain-anthropic.
Stage 1 of the LangGraph multi-agent build: install packages and set your API key
Start with a clean virtual environment. LangGraph’s Python docs document installation from the main site, and Anthropic model integration is documented in LangChain’s provider page. Install the packages below, then export your API key.
If you are using macOS or Linux, the shell commands below are enough. On Windows PowerShell, set the environment variable with $env:ANTHROPIC_API_KEY=".." instead.
python -m venv .venv
source .venv/bin/activate
pip install -U langgraph langchain langchain-anthropic
export ANTHROPIC_API_KEY="your_api_key_here"
Stage 2: Define the LangGraph multi-agent shared state with TypedDict
LangGraph works best when you make state explicit. Instead of passing loose dictionaries between prompts, define a TypedDict that names the fields every node can read or update. This is the backbone of the workflow.
Our state tracks the user topic, the researcher’s notes, the current draft, editorial feedback, a revision counter, and a final status. The revision counter matters because it gives the graph a hard stop. Without it, an editor loop can run forever if the model keeps asking for changes.
⚠️ Why state discipline matters. Most multi-agent demos become hard to debug because each agent invents its own scratchpad. A single typed state makes routing and checkpointing much easier to reason about.
from typing import Literal
from typing_extensions import TypedDict
class CrewState(TypedDict):
topic: str
research_notes: str
draft: str
editor_feedback: str
revision_count: int
status: Literal["researching", "writing", "editing", "approved", "needs_revision"]
Stage 3: Create the Claude model and the three agents
Next, instantiate Claude through langchain-anthropic. LangChain’s Anthropic integration docs show the ChatAnthropic interface, which behaves like other chat models in the LangChain ecosystem. We’ll use one model instance and vary behavior through system prompts.
Each node function receives the current state and returns a partial state update. That is the key LangGraph pattern. The researcher writes concise notes with uncertainties called out. The writer turns notes into a clean draft. The editor returns a machine-readable decision so the graph can route deterministically.
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, SystemMessage
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
def researcher_node(state: CrewState) -> CrewState:
messages = [
SystemMessage(
content=(
"You are a careful researcher. Produce concise research notes on the user's topic. "
"Include key points, useful context, and a short section called 'Caveats' listing "
"uncertainties or claims that need verification."
)
),
HumanMessage(content=f"Topic: {state['topic']}")
]
response = model.invoke(messages)
return {
"research_notes": response.content,
"status": "writing"
}
def writer_node(state: CrewState) -> CrewState:
messages = [
SystemMessage(
content=(
"You are a clear technical writer. Using the research notes and any editor feedback, "
"write a concise article draft with a title and 3 short sections. "
"If editor feedback exists, revise accordingly."
)
),
HumanMessage(
content=(
f"Topic: {state['topic']}\n\n"
f"Research notes:\n{state['research_notes']}\n\n"
f"Editor feedback:\n{state['editor_feedback']}"
)
)
]
response = model.invoke(messages)
return {
"draft": response.content,
"status": "editing"
}
def editor_node(state: CrewState) -> CrewState:
messages = [
SystemMessage(
content=(
"You are a strict editor. Review the draft for clarity, structure, and factual caution. "
"Respond in exactly this format:\n"
"DECISION: approve OR revise\n"
"FEEDBACK: <short feedback>"
)
),
HumanMessage(
content=(
f"Topic: {state['topic']}\n\n"
f"Research notes:\n{state['research_notes']}\n\n"
f"Draft:\n{state['draft']}"
)
)
]
response = model.invoke(messages)
text = response.content if isinstance(response.content, str) else str(response.content)
decision = "revise"
feedback = text
for line in text.splitlines():
if line.upper().startswith("DECISION:"):
decision = line.split(":", 1)[1].strip().lower()
if line.upper().startswith("FEEDBACK:"):
feedback = line.split(":", 1)[1].strip()
if decision == "approve":
return {
"editor_feedback": feedback,
"status": "approved"
}
return {
"editor_feedback": feedback,
"revision_count": state["revision_count"] + 1,
"status": "needs_revision"
}
“The most useful LangGraph habit is to make nodes return state updates, not side effects.”
Practical pattern from LangGraph’s stateful workflow model
Stage 4: Build the LangGraph multi-agent graph with add_node, add_edge, and conditional routing
Now wire the agents into a StateGraph. This is where LangGraph differs from higher-level agent frameworks that hide control flow. You define the nodes, connect the normal path with add_edge, and then use conditional routing after the editor step. In current LangGraph Python usage, that routing is done with add_conditional_edges.
The routing function below checks whether the editor approved the draft. If yes, the graph ends. If not, it loops back to the writer unless the revision limit has been reached. That gives you a practical version of a multi-agent review cycle without introducing a supervisor agent.
from langgraph.graph import END, START, StateGraph
def route_after_editor(state: CrewState) -> str:
if state["status"] == "approved":
return "end"
if state["revision_count"] >= 2:
return "end"
return "writer"
graph_builder = StateGraph(CrewState)
graph_builder.add_node("researcher", researcher_node)
graph_builder.add_node("writer", writer_node)
graph_builder.add_node("editor", editor_node)
graph_builder.add_edge(START, "researcher")
graph_builder.add_edge("researcher", "writer")
graph_builder.add_edge("writer", "editor")
graph_builder.add_conditional_edges(
"editor",
route_after_editor,
{
"writer": "writer",
"end": END,
},
)
| Node | Reads | Writes | Next step |
|---|---|---|---|
| researcher | topic | research_notes, status | writer |
| writer | topic, research_notes, editor_feedback | draft, status | editor |
| editor | topic, research_notes, draft | editor_feedback, status, revision_count | END or writer |
Stage 5: Add LangGraph multi-agent checkpointing with MemorySaver
Checkpointing is one of the reasons teams reach for LangGraph instead of plain chains. LangGraph supports persistence so a graph can resume from saved state. For a local tutorial, MemorySaver is the simplest checkpointer. It stores thread state in memory for the life of the process.
This is enough to demonstrate the pattern: compile the graph with a checkpointer, then invoke it with a thread_id in the config. The docs cover persistence and memory concepts from the LangGraph site. In production, you would usually move from in-memory storage to a durable backend.
📌 Why this matters. A checkpointed graph is easier to inspect, resume, and eventually connect to human approval steps than a one-shot prompt chain.
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
app = graph_builder.compile(checkpointer=memory)
Stage 6: Run the LangGraph multi-agent workflow end to end
With the graph compiled, invoke it with an initial state. The only required user input here is the topic. Everything else starts empty. We also pass a thread ID so the checkpointer can associate state with a specific run.
The result is a final state dictionary. Print the research notes, draft, editor feedback, and status so you can see what happened. If the editor asked for revisions, the graph will have looped through the writer again before returning.
initial_state: CrewState = {
"topic": "How AI coding agents are changing software delivery",
"research_notes": "",
"draft": "",
"editor_feedback": "",
"revision_count": 0,
"status": "researching",
}
result = app.invoke(
initial_state,
config={"configurable": {"thread_id": "demo-thread-1"}}
)
print("\n=== FINAL STATUS ===")
print(result["status"])
print("\n=== RESEARCH NOTES ===")
print(result["research_notes"])
print("\n=== DRAFT ===")
print(result["draft"])
print("\n=== EDITOR FEEDBACK ===")
print(result["editor_feedback"])
print("\n=== REVISIONS ===")
print(result["revision_count"])
Stage 7: Full working example in one file
If you want the shortest path from tutorial to terminal, put the full script below into app.py and run it with python app.py. This combines the state definition, node functions, graph wiring, checkpointing, and invocation.
The code is intentionally compact. It uses only the APIs needed for this build and leaves room for you to add tools, structured outputs, or a human review node later.
from typing import Literal
from typing_extensions import TypedDict
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
class CrewState(TypedDict):
topic: str
research_notes: str
draft: str
editor_feedback: str
revision_count: int
status: Literal["researching", "writing", "editing", "approved", "needs_revision"]
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
def researcher_node(state: CrewState) -> CrewState:
response = model.invoke([
SystemMessage(
content=(
"You are a careful researcher. Produce concise research notes on the user's topic. "
"Include key points, useful context, and a short section called 'Caveats' listing "
"uncertainties or claims that need verification."
)
),
HumanMessage(content=f"Topic: {state['topic']}")
])
return {
"research_notes": response.content,
"status": "writing"
}
def writer_node(state: CrewState) -> CrewState:
response = model.invoke([
SystemMessage(
content=(
"You are a clear technical writer. Using the research notes and any editor feedback, "
"write a concise article draft with a title and 3 short sections. "
"If editor feedback exists, revise accordingly."
)
),
HumanMessage(
content=(
f"Topic: {state['topic']}\n\n"
f"Research notes:\n{state['research_notes']}\n\n"
f"Editor feedback:\n{state['editor_feedback']}"
)
)
])
return {
"draft": response.content,
"status": "editing"
}
def editor_node(state: CrewState) -> CrewState:
response = model.invoke([
SystemMessage(
content=(
"You are a strict editor. Review the draft for clarity, structure, and factual caution. "
"Respond in exactly this format:\n"
"DECISION: approve OR revise\n"
"FEEDBACK: <short feedback>"
)
),
HumanMessage(
content=(
f"Topic: {state['topic']}\n\n"
f"Research notes:\n{state['research_notes']}\n\n"
f"Draft:\n{state['draft']}"
)
)
])
text = response.content if isinstance(response.content, str) else str(response.content)
decision = "revise"
feedback = text
for line in text.splitlines():
if line.upper().startswith("DECISION:"):
decision = line.split(":", 1)[1].strip().lower()
if line.upper().startswith("FEEDBACK:"):
feedback = line.split(":", 1)[1].strip()
if decision == "approve":
return {
"editor_feedback": feedback,
"status": "approved"
}
return {
"editor_feedback": feedback,
"revision_count": state["revision_count"] + 1,
"status": "needs_revision"
}
def route_after_editor(state: CrewState) -> str:
if state["status"] == "approved":
return "end"
if state["revision_count"] >= 2:
return "end"
return "writer"
def build_app():
graph_builder = StateGraph(CrewState)
graph_builder.add_node("researcher", researcher_node)
graph_builder.add_node("writer", writer_node)
graph_builder.add_node("editor", editor_node)
graph_builder.add_edge(START, "researcher")
graph_builder.add_edge("researcher", "writer")
graph_builder.add_edge("writer", "editor")
graph_builder.add_conditional_edges(
"editor",
route_after_editor,
{
"writer": "writer",
"end": END,
},
)
memory = MemorySaver()
return graph_builder.compile(checkpointer=memory)
if __name__ == "__main__":
app = build_app()
initial_state: CrewState = {
"topic": "How AI coding agents are changing software delivery",
"research_notes": "",
"draft": "",
"editor_feedback": "",
"revision_count": 0,
"status": "researching",
}
result = app.invoke(
initial_state,
config={"configurable": {"thread_id": "demo-thread-1"}},
)
print("\n=== FINAL STATUS ===")
print(result["status"])
print("\n=== RESEARCH NOTES ===")
print(result["research_notes"])
print("\n=== DRAFT ===")
print(result["draft"])
print("\n=== EDITOR FEEDBACK ===")
print(result["editor_feedback"])
print("\n=== REVISIONS ===")
print(result["revision_count"])
Stage 8: What to watch for when adapting the LangGraph multi-agent pattern
A few practical notes will save you time. First, keep node outputs narrow. The more free-form text you ask one agent to generate for another, the more brittle the handoff becomes. If you need stronger guarantees, move the editor to structured output and parse a schema instead of line-based text.
Second, cap revision loops. In this tutorial the limit is two revisions. That is not arbitrary busywork; it is a guardrail against runaway cost and latency. Third, separate orchestration from prompting. LangGraph should own state transitions and persistence. Prompts should stay focused on the role of each node.
If you want to compare this explicit graph approach with a higher-level crew abstraction, our CrewAI walkthrough is the natural companion read. If you want a broader architectural overview of LangGraph itself, start with our LangGraph guide and then go back to the official docs at langchain-ai.github.io/langgraph.
📌 Where to go from here. The next useful upgrades are tool-enabled research, structured editor decisions, a human approval node, and a durable checkpointer instead of in-memory state.
Frequently asked questions
What makes LangGraph a good fit for multi-agent systems?
LangGraph is designed for stateful workflows, which makes it a strong fit for multi-agent systems where different nodes need to read and update shared context over time. The official docs describe it as a framework for building stateful, multi-actor applications with LLMs; see the LangGraph documentation.
Do I need a separate model for each agent in this tutorial?
No. This tutorial uses one Claude chat model instance and changes behavior with role-specific prompts. That is often enough for a first build. The Anthropic integration in LangChain shows how to instantiate Claude models through langchain-anthropic.
Why use MemorySaver if it is not durable?
MemorySaver is the simplest way to learn LangGraph checkpointing because it lets you compile a graph with a checkpointer and associate runs with a thread ID. It is useful for local development and demos. For broader persistence concepts and production-oriented patterns, start with the LangGraph docs.
Primary sources
- LangGraph documentation — LangChain
- LangChain Anthropic integration docs — LangChain
- Anthropic API documentation — Anthropic
Last updated: May 20, 2026. Related: Agent Infrastructure.