.jpg?alt=media&token=fa69b8b3-19be-4147-9583-843a5bdcffcb)
What you'll learn: How to build AI agent systems with LangGraph - from basic concepts to working code. We'll create an article-writing pipeline with multiple AI agents that collaborate, review each other's work, and iterate until the result is perfect.
PART 1: Basic Concepts
What is a Graph?
Before diving into LangGraph, it's essential to understand what a graph is in programming.
A Graph is a Data Structure
Imagine a subway map:
- Stations are the nodes
- Lines between stations are the edges
1 Typical Graph: Subway Map (Analogy):
2
3 [A] [Victory Square]
4 │ │
5 ▼ ▼
6 [B]───────►[C] [October]───►[Kupala]
7 │ │
8 ▼ ▼
9 [D] [Cultural Institute]
In LangGraph:
- Nodes are functions (agents) that perform tasks.
- Edges are rules indicating the order in which tasks are executed.
Why is LangGraph Needed?
Problem: Traditional AI Programs are Linear
1Typical Chain:
2
3 Question → LLM → Answer
4
5 or
6
7 Question → Tool 1 → LLM → Tool 2 → Answer
This works for simple tasks, but what if:
- You need to go back and redo something?
- You need to verify the result and possibly repeat?
- You need multiple agents with different roles?
Solution: LangGraph Allows Creating Cycles
1LangGraph (Graph):
2
3 ┌──────────────────────┐
4 │ │
5 ▼ │
6 Question → [Researcher] → [Writer] → [Reviewer]
7 │
8 ▼
9 Good? ───NO───► back to Writer
10 │
11 YES
12 │
13 ▼
14 [Result]
What is "state"?
State is the memory of the program.
Imagine you are writing an article with a team:
- The researcher gathered materials → needs to write them down somewhere.
- The writer created a draft → needs to pass it to the reviewer.
- The reviewer wrote comments → needs to return them to the writer.
State is like a "basket" where everyone puts their results and from which they take data.
1┌─────────────────────────────────────────────────────────────┐
2│ STATE (Стан) │
3├─────────────────────────────────────────────────────────────┤
4│ │
5│ topic: "What I Write" │
6│ research: "Materials from the Researcher" │
7│ draft: "Draft from the Writer" │
8│ review: "Comments from the Reviewer" │
9│ finalArticle: "Final Article" │
10│ messages: [history of all messages] │
11│ │
12└─────────────────────────────────────────────────────────────┘
13 ▲ ▲ ▲
14 │ │ │
15 Researcher Writer Reviewer
16 (reads and writes) (reads and writes) (reads and writes)
PART 2: Annotation - How State is Created
What is Annotation?
Annotation is a way of describing the structure of a state. It is like creating a form with fields.
Analogy: Survey
Imagine you are creating a survey:
1┌─────────────────────────────────────────┐
2│ EMPLOYEE SURVEY │
3├─────────────────────────────────────────┤
4│ Name: ___________________ │
5│ Age: _____ │
6│ Work Experience (years): _____ │
7│ Skills List: ___, ___, ___ │
8└─────────────────────────────────────────┘
In code, it looks like this:
1// ANALOGY: Creating a survey form
2const Survey = {
3 name: '', // Text field
4 age: 0, // Number
5 experience: 0, // Number
6 skills: [], // List
7};
In LangGraph: Annotation.Root
In LangGraph, state is created using Annotation.Root(). This is a special function that describes the structure of your data - what fields exist and how they should be updated.
1import { Annotation } from '@langchain/langgraph';
2
3const ResearchState = Annotation.Root({
4 // Each field is described through Annotation<Type>
5 topic: Annotation<string>({...}),
6 research: Annotation<string>({...}),
7 draft: Annotation<string>({...}),
8 // etc.
9});
Each field inside Annotation.Root has two important properties: reducer (how to update the value) and default (initial value). We'll explore these next.
What is a Reducer?
Problem: How to merge data?
What should you do when multiple agents write to the same field?
1Agent 1 writes: topic = "Topic A"
2Agent 2 writes: topic = "Topic B"
3
4What should be in topic? "Topic A"? "Topic B"? "Topic A + Topic B"?
Solution: Reducer - a function that resolves
Reducer is a function that takes:
- Current value (what is already there)
- New Value (what comes in)
It returns: result (what to keep)
1reducer: (current, update) => result;
2// ▲ ▲ ▲
3// │ │ └── What will be kept
4// │ └── New value
5// └── Current value
Examples of Reducers:
1. Reducer "REPLACE"
The new value completely replaces the old one.
1// Code:
2reducer: (current, update) => update;
3
4// Example:
5// Current: "Theme A"
6// New: "Theme B"
7// Result: "Theme B" ← new replaces old
Analogy: You overwrite a file - the new content replaces the old one.
1file.txt
2──────────────
3Was: "Old text"
4 ▼ (overwrite)
5Is: "New text"
2. Reducer "APPEND"
New elements are added to the existing ones.
1// Code:
2reducer: (current, update) => [...current, ...update];
3
4// Example:
5// Current: ["Message 1", "Message 2"]
6// New: ["Message 3"]
7// Result: ["Message 1", "Message 2", "Message 3"]
Analogy: You add new entries to a diary - the old ones remain.
1diary.txt
2──────────────
3Was: "Monday: did A"
4 "Tuesday: did B"
5 ▼ (adding)
6Became: "Monday: did A"
7 "Tuesday: did B"
8 "Wednesday: did C" ← added
Full example of the state:
1const ResearchState = Annotation.Root({
2 // ═══════════════════════════════════════════════════════
3 // FIELD: messages (messages)
4 // ═══════════════════════════════════════════════════════
5 messages: Annotation<BaseMessage[]>({
6 // REDUCER: Add new messages to existing ones
7 reducer: (current, update) => [...current, ...update],
8 // DEFAULT: Initial value - empty array
9 default: () => [],
10 }),
11 //
12 // How it works:
13 // 1. Beginning: messages = []
14 // 2. Researcher: messages = [] + [AIMessage] = [AIMessage]
15 // 3. Writer: messages = [AIMessage] + [AIMessage] = [AIMessage, AIMessage]
16 // 4. And so on - all messages are stored
17
18 // ═══════════════════════════════════════════════════════
19 // FIELD: topic (topic)
20 // ═══════════════════════════════════════════════════════
21 topic: Annotation<string>({
22 // REDUCER: Replace old value with new one
23 reducer: (_, update) => update, // "_" means "ignore"
24 default: () => '',
25 }),
26 //
27 // How it works:
28 // 1. Beginning: topic = ""
29 // 2. User: topic = "" → "LangChain"
30 // If someone else writes in topic - the old value will disappear
31
32 // ═══════════════════════════════════════════════════════
33 // FIELD: iterationCount (iteration counter)
34 // ═══════════════════════════════════════════════════════
35 iterationCount: Annotation<number>({
36 reducer: (_, update) => update, // Simply replace
37 default: () => 0, // Start from 0
38 }),
39 //
40 // The writer writes each time: iterationCount: state.iterationCount + 1
41 // 1. Start: iterationCount = 0
42 // 2. Writer (1): iterationCount = 0 + 1 = 1
43 // 3. Writer (2): iterationCount = 1 + 1 = 2
44 // 4. Writer (3): iterationCount = 2 + 1 = 3
45});
Visualization of Reducers:
1┌─────────────────────────────────────────────────────────────────┐
2│ REDUCER: REPLACE │
3├─────────────────────────────────────────────────────────────────┤
4│ │
5│ Current: █████████████ "Old text" │
6│ ▼ │
7│ New: ░░░░░░░░░░░░░ "New text" │
8│ ▼ │
9│ Result: ░░░░░░░░░░░░░ "New text" ← only new │
10│ │
11└─────────────────────────────────────────────────────────────────┘
12
13┌─────────────────────────────────────────────────────────────────┐
14│ REDUCER: ADD │
15├─────────────────────────────────────────────────────────────────┤
16│ │
17│ Current: [█] [█] [█] ← three elements │
18│ + │
19│ New: [░] [░] ← two new │
20│ = │
21│ Result: [█] [█] [█] [░] [░] ← all together │
22│ │
23└─────────────────────────────────────────────────────────────────┘
What is default?
default is a function that returns the initial value of a field.
Why a function and not just a value?
This is a common JavaScript pitfall. If you use a plain object or array as default, all instances will share the same reference - changes in one place will affect all others!
1// POOR: If this is an object or array
2default: [] // All instances will refer to the same array!
3
4// GOOD: The function creates a new array each time
5default: () => [] // Each instance will get its own array
Examples:
1// For a string
2default: () => '' // Empty string
3
4// For a number
5default: () => 0 // Zero
6
7// For an array
8default: () => [] // Empty array
9
10// For an object
11default: () => ({}) // Empty object
12
13// For boolean
14default: () => false // false
PART 3: Nodes - Agents
What is a node?
Node is a function that:
- Receives the full state
- Performs some work (for example, calls LLM)
- Returns a partial state update
Analogy: Worker on the Assembly Line
1┌─────────────────────────────────────────────────────────────┐
2│ ASSEMBLY LINE │
3├─────────────────────────────────────────────────────────────┤
4│ │
5│ [Box] ──► [Worker 1] ───────► [Worker 2] ──────► │
6│ │ │ │
7│ ▼ ▼ │
8│ Adds part A Adds part B │
9│ │
10└─────────────────────────────────────────────────────────────┘
11
12Each worker:
131. Sees what has already been done (state)
142. Does their part of the work
153. Passes it on with additions
Node Structure in Code:
1async function myNode(
2 state: ResearchStateType // ← INPUT: Full state
3): Promise<Partial<ResearchStateType>> {
4 // ← OUTPUT: Partial update
5 // 1. Read data from state
6 const data = state.someField;
7
8 // 2. Do the work
9 const result = await doSomething(data);
10
11 // 3. Returning the update (only what has changed)
12 return {
13 someField: result,
14 };
15}
Important: Partial<State>
A node doesn't need to return the entire state - only the fields that changed. LangGraph will merge your partial update with the existing state using the reducers you defined.
1// The full state has 6 fields:
2state = {
3 topic: "...",
4 research: "...",
5 draft: "...",
6 review: "...",
7 finalArticle: "...",
8 messages: [...],
9 iterationCount: 0
10}
11
12// But the node can only return what it has changed:
13return {
14 draft: "New draft", // Changed
15 messages: [new AIMessage("...")], // Added
16 // The other fields are not mentioned - they will remain as they were
17}
Our nodes in detail
Now let's look at the four nodes in our article-writing system. Each node has a specific role and passes its results to the next one through the shared state.
Node 1: Researcher (researcherNode)
The researcher is the first agent in our pipeline. It takes the topic and generates research materials that will be used by the writer.
1async function researcherNode(
2 state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4 // ┌─────────────────────────────────────────────────────────────┐
5 // │ STEP 1: Get the topic for research │
6 // └─────────────────────────────────────────────────────────────┘
7 const topic =
8 state.topic ||
9 String(state.messages[state.messages.length - 1]?.content) ||
10 '';
11 // ▲ ▲
12 // │ └── Or the last message
13 // └── First, try to take topic
14
15 // ┌─────────────────────────────────────────────────────────────┐
16 // │ STEP 2: Form the prompt for LLM │
17 // └─────────────────────────────────────────────────────────────┘
18 const prompt = `You are an expert researcher. Your task is to gather key information.
19 Topic: ${topic}
20 Conduct a brief research...`;
21
22 // ┌─────────────────────────────────────────────────────────────┐
23 // │ STEP 3: Call LLM │
24 // └─────────────────────────────────────────────────────────────┘
25 const response = await model.invoke([
26 { role: 'system', content: prompt }, // Instructions for AI
27 { role: 'user', content: `Research the topic: ${topic}` }, // Request
28 ]);
29
30 const research = String(response.content); // Result from LLM
31
32 // ┌─────────────────────────────────────────────────────────────┐
33 // │ STEP 4: Return state update │
34 // └─────────────────────────────────────────────────────────────┘
35 return {
36 research, // Store the research result
37 messages: [
38 new AIMessage({
39 content: `[Research completed]
40${research}`,
41 }),
42 ],
43 // ▲ Add a message to the history
44 };
45}
What happens:
1INPUT (state): OUTPUT (update):
2┌─────────────────────┐ ┌─────────────────────┐
3│ topic: "LangChain" │ │ research: "LangCh.. │
4│ research: "" │ ──────► │ messages: [+1 msg] │
5│ draft: "" │ └─────────────────────┘
6│ messages: [1 msg] │
7└─────────────────────┘
8 ▼ After merging:
9
10 ┌─────────────────────┐
11 │ topic: "LangChain" │
12 │ research: "LangCh...│ ← updated
13 │ draft: "" │
14 │ messages: [2 msgs] │ ← added
15 └─────────────────────┘
Node 2: Writer (writerNode)
The writer takes the research and creates an article draft. If this is a revision (after reviewer feedback), it also considers the review comments. Notice how iterationCount helps us track how many times the article has been rewritten.
1async function writerNode(
2 state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4 // ┌─────────────────────────────────────────────────────────────┐
5 // │ STEP 1: Read the research and possible review │
6 // └─────────────────────────────────────────────────────────────┘
7 const prompt = `You are a technical writer. Based on the research, write an article.
8
9Research:
10${state.research}
11
12${state.review ? `Previous review (consider comments): ${state.review}` : ''}
13`;
14
15 // ┌─────────────────────────────────────────────────────────────┐
16 // │ STEP 2: Call LLM │
17 // └─────────────────────────────────────────────────────────────┘
18 const response = await model.invoke([
19 { role: 'system', content: prompt },
20 { role: 'user', content: 'Write an article based on the research' },
21 ]);
22
23 const draft = String(response.content);
24
25 // ┌─────────────────────────────────────────────────────────────┐
26 // │ STEP 3: Return the update │
27 // └─────────────────────────────────────────────────────────────┘
28 return {
29 draft, // Draft of the article
30 iterationCount: state.iterationCount + 1, // Increment the counter
31 messages: [
32 new AIMessage({ content: `[Draft ${state.iterationCount + 1}]` }),
33 ],
34 };
35}
What happens on repeat call:
1FIRST CALL: SECOND CALL (after review):
2┌─────────────────────┐ ┌─────────────────────┐
3│ research: "..." │ │ research: "..." │
4│ review: "" │ │ review: "Refine..." │ ← there are comments!
5│ iterationCount: 0 │ │ iterationCount: 1 │
6└─────────────────────┘ └─────────────────────┘
7 │ │
8 ▼ ▼
9Writing without comments Considering comments
10 │ │
11 ▼ ▼
12┌─────────────────────┐ ┌─────────────────────┐
13│ draft: "Version 1" │ │ draft: "Version 2" │
14│ iterationCount: 1 │ │ iterationCount: 2 │
15└─────────────────────┘ └─────────────────────┘
Node 3: Reviewer (reviewerNode)
The reviewer evaluates the draft and decides if it's ready for publication. The key here is the output: if the review contains "APPROVED", the article moves to finalization. Otherwise, it goes back to the writer for improvements. This is what enables the cycle in our graph.
1async function reviewerNode(state: ResearchStateType): Promise<Partial<ResearchStateType>> {
2
3 // ┌─────────────────────────────────────────────────────────────┐
4 // │ STEP 1: Formulating the review request │
5 // └─────────────────────────────────────────────────────────────┘
6 const prompt = `You are a strict editor. Evaluate the article.
7
8Article:
9${state.draft}
10
11Evaluate based on the criteria:
121. Accuracy of information
132. Structure and logic
143. Language quality
154. Completeness of the topic coverage
16
17If the article is good - say "APPROVED". When improvements are needed, please provide specific recommendations.
18`;
19
20 // ┌─────────────────────────────────────────────────────────────┐
21 // │ STEP 2: Get the review │
22 // └─────────────────────────────────────────────────────────────┘
23 const response = await model.invoke([...]);
24 const review = String(response.content);
25
26 // ┌─────────────────────────────────────────────────────────────┐
27 // │ STEP 3: Return the review │
28 // └─────────────────────────────────────────────────────────────┘
29 return {
30 review, // Review (either "APPROVED" or comments)
31 messages: [new AIMessage({ content: `[Review]\n${review}` })],
32 };
33}
Two possible outcomes:
1OPTION A: The article is good OPTION B: Needs improvement
2┌───────────────────────────┐ ┌───────────────────────────┐
3│ review: "APPROVED. │ │ review: "Improve: │
4│ The article is excellent!"│ │ 1. Add examples │
5│ │ │ 2. Clarify the terms" │
6└───────────────────────────┘ └───────────────────────────┘
7 │ │
8 ▼ ▼
9 Moving to Returning to
10 finalizer writer
Node 4: Finalizer (finalizerNode)
The finalizer is the simplest node - it just copies the approved draft to the finalArticle field, marking the end of our workflow. This is called when the reviewer approves the article.
1async function finalizerNode(
2 state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4 // Simple node - copies the draft to the final article
5 return {
6 finalArticle: state.draft, // Final article
7 messages: [new AIMessage({ content: `[READY]\n\n${state.draft}` })],
8 };
9}
What happens:
1INPUT: OUTPUT:
2┌─────────────────────┐ ┌─────────────────────┐
3│ draft: "Ready..." │ ──────► │ finalArticle: │
4│ finalArticle: "" │ │ "Ready..." │
5└─────────────────────┘ └─────────────────────┘
PART 4: Edges
What is an edge?
Edge is a rule that states: "After this node, the following node is executed."
Analogy: Arrows on the Diagram
1Recipe steps:
2
3 [Make dough] ────► [Add sauce] ────► [Add cheese] ────► [Bake]
4 │ │ │ │
5 ▼ ▼ ▼ ▼
6 TRANSITION TRANSITION TRANSITION END
Two Types of Transitions in LangGraph:
1. Simple Transitions (addEdge)
Always go to the same node.
1workflow.addEdge('researcher', 'writer');
2// ▲ ▲
3// │ └── Where are we going
4// └── From where are we coming
5
6// Meaning: After researcher, we ALWAYS go to writer
Visualization:
1 [researcher] ───────────────► [writer]
2 (always)
2. Conditional Transitions (addConditionalEdges)
The choice of the next node depends on the state.
1workflow.addConditionalEdges(
2 'reviewer', // Source node
3 shouldContinue, // Function that decides where to go
4 {
5 writer: 'writer', // If the function returns 'writer' → go to writer
6 finalizer: 'finalizer', // If the function returns 'finalizer' → go to finalizer
7 }
8);
Visualization:
1 ┌──────────► [writer]
2 │
3 [reviewer] ─────► [?] ──┤
4 │
5 └──────────► [finalizer]
6
7 The function shouldContinue decides which arrow to take.
Detailed Function shouldContinue
This is the "brain" of our conditional edge. It examines the current state and decides where to go next. The function must return a string that matches one of the keys in the mapping object we defined in addConditionalEdges.
1function shouldContinue(state: ResearchStateType): 'writer' | 'finalizer' {
2 // ▲
3 // └── Returns one of these strings
4
5 const review = state.review.toLowerCase();
6 const maxIterations = 3;
7
8 // ═══════════════════════════════════════════════════════════════
9 // CONDITION 1: Reviewer said "APPROVED"
10 // ═══════════════════════════════════════════════════════════════
11 if (review.includes('зацверджана') || review.includes('approved')) {
12 console.log('Article approved');
13 return 'finalizer'; // ← Proceed to finalize
14 }
15
16 // ═══════════════════════════════════════════════════════════════
17 // CONDITION 2: Too many attempts (protection against infinite loop)
18 // ═══════════════════════════════════════════════════════════════
19 if (state.iterationCount >= maxIterations) {
20 console.log('Maximum iterations reached');
21 return 'finalizer'; // ← Force finalize
22 }
23
24 // ═══════════════════════════════════════════════════════════════
25 // OTHERWISE: Needs revision
26 // ═══════════════════════════════════════════════════════════════
27 console.log('Revision needed');
28 return 'writer'; // ← Go back to writer
29}
Decision flowchart:
1 ┌───────────────────┐
2 │ shouldContinue │
3 │ (function) │
4 └─────────┬─────────┘
5 │
6 ▼
7 ┌──────────────────────────────┐
8 │ Is "approved" present │
9 │ in the review? │
10 └──────────────┬───────────────┘
11 │
12 ┌────────────┴────────────┐
13 │ │
14 YES NO
15 │ │
16 ▼ ▼
17 ┌────────────┐ ┌──────────────────────────┐
18 │ return │ │ Is iterationCount >= 3? │
19 │'finalizer' │ └──────────────┬───────────┘
20 │ │ │
21 └────────────┘ ┌────────────┴────────────┐
22 │ │
23 YES NO
24 │ │
25 ▼ ▼
26 ┌────────────┐ ┌────────────┐
27 │ return │ │ return │
28 │'finalizer' │ │ 'writer' │
29 │ │ │ │
30 └────────────┘ └────────────┘
How the graph is built
Now let's put everything together! Building a graph in LangGraph follows a simple pattern: create the graph, add nodes, connect them with edges, and compile.
Sequence of creation:
1// ═══════════════════════════════════════════════════════════════
2// STEP 1: Create StateGraph with our state
3// ═══════════════════════════════════════════════════════════════
4const workflow = new StateGraph(ResearchState)
5
6 // ═══════════════════════════════════════════════════════════
7 // STEP 2: Add nodes (register functions)
8 // ═══════════════════════════════════════════════════════════
9 .addNode('researcher', researcherNode) // Name 'researcher' → function
10 .addNode('writer', writerNode) // Name 'writer' → function
11 .addNode('reviewer', reviewerNode) // Name 'reviewer' → function
12 .addNode('finalizer', finalizerNode) // Name 'finalizer' → function
13
14 // ═══════════════════════════════════════════════════════════
15 // STEP 3: Add simple transitions
16 // ═══════════════════════════════════════════════════════════
17 .addEdge('__start__', 'researcher') // Start → Researcher
18 .addEdge('researcher', 'writer') // Researcher → Writer
19 .addEdge('writer', 'reviewer') // Writer → Reviewer
20
21 // ═══════════════════════════════════════════════════════════
22 // STEP 4: Add conditional transition
23 // ═══════════════════════════════════════════════════════════
24 .addConditionalEdges('reviewer', shouldContinue, {
25 writer: 'writer', // If 'writer' → back to writer
26 finalizer: 'finalizer', // If 'finalizer' → to finalizer
27 })
28
29 // ═══════════════════════════════════════════════════════════
30 // STEP 5: Add final transition
31 // ═══════════════════════════════════════════════════════════
32 .addEdge('finalizer', '__end__'); // Finalizer → End
Special nodes:
1┌───────────────┬─────────────────────────────────────────────┐
2│ '__start__' │ Virtual start node │
3│ │ LangGraph automatically starts from it │
4├───────────────┼─────────────────────────────────────────────┤
5│ '__end__' │ Virtual end node │
6│ │ When reached - the graph is completed │
7└───────────────┴─────────────────────────────────────────────┘
PART 5: Compilation and execution
We've defined our state, nodes, and edges. Now it's time to turn this blueprint into a running application!
What is compile()?
compile() is the process of transforming a graph description into an executable program. Until you call compile(), you just have a description of what you want to build - not an actual runnable system.
1// Graph description (blueprint)
2const workflow = new StateGraph(ResearchState)
3 .addNode(...)
4 .addEdge(...);
5
6// Compilation into an executable program
7const app = workflow.compile();
8// ▲
9// └── Now this can be run!
Analogy: Recipe vs. Finished Dish
1workflow (description) app (compiled program)
2────────────────────── ──────────────────────
3Recipe on paper Ready pizza
4 - How to make the dough (can be eaten)
5 - What to add
6 - How to bake
7
8Cannot be eaten! Can be eaten!
Checkpointer (MemorySaver)
What is it?
Checkpointer is a mechanism for saving state after each step.
1import { MemorySaver } from '@langchain/langgraph';
2
3const checkpointer = new MemorySaver();
4
5const app = workflow.compile({
6 checkpointer, // ← Adding checkpointer
7});
Why is this needed?
1WITHOUT CHECKPOINTER:
2═════════════════════════════════════════════════════════════════
3
4 Run 1: START → researcher → writer → ... → END
5
6 Run 2: START → researcher → ... (everything from the beginning!)
7
8 ✗ Cannot continue from the stopping point
9 ✗ Cannot view history
10
11
12WITH CHECKPOINTER:
13═════════════════════════════════════════════════════════════════
14
15 Run 1: START → researcher → [SAVED]
16 │
17 thread_id: "chat-123"
18
19 Run 2: [RESUMING] → writer → reviewer → ...
20 │
21 thread_id: "chat-123"
22
23 ✓ Can continue from the stopping point
24 ✓ Can view history
25 ✓ Can have multiple independent "conversations"
Thread ID - identifier of the "thread"
1const config = {
2 configurable: {
3 thread_id: 'article-1', // Unique ID for this "conversation"
4 },
5};
6
7// Each thread_id is a separate story
8// 'article-1' - about one article
9// 'article-2' - about another article
10// They do not overlap!
Running the Graph
Finally! Let's run our graph. The invoke() function takes initial state values and configuration, then executes the entire graph from start to finish.
The invoke() Function
1const result = await app.invoke(
2 // Initial data for the state
3 {
4 topic: 'Benefits of LangChain',
5 messages: [new HumanMessage('Benefits of LangChain')],
6 },
7 // Configuration
8 {
9 configurable: {
10 thread_id: 'article-1',
11 },
12 }
13);
What Happens When invoke() is Called:
1┌─────────────────────────────────────────────────────────────────┐
2│ invoke() │
3└───────────────────────────────┬─────────────────────────────────┘
4 │
5 ▼
6┌─────────────────────────────────────────────────────────────────┐
7│ 1. INITIALIZATION OF STATE │
8│ state = { │
9│ topic: "Advantages of LangChain", │
10│ messages: [HumanMessage], │
11│ research: "", ← default │
12│ draft: "", ← default │
13│ review: "", ← default │
14│ finalArticle: "", ← default │
15│ iterationCount: 0, ← default │
16│ } │
17└───────────────────────────────┬─────────────────────────────────┘
18 │
19 ▼
20┌─────────────────────────────────────────────────────────────────┐
21│ 2. BEGINNING: __start__ → researcher │
22│ Executing researcherNode(state) │
23│ → Receiving update {research: "...", messages: [...]} │
24│ → Merging with state through reducers │
25│ Saving checkpoint │
26└───────────────────────────────┬─────────────────────────────────┘
27 │
28 ▼
29┌─────────────────────────────────────────────────────────────────┐
30│ 3. TRANSITION: researcher → writer │
31│ Execute writerNode(state) │
32│ → Receive update {draft: "...", iterationCount: 1} │
33│ → Merge with state │
34│ Save checkpoint │
35└───────────────────────────────┬─────────────────────────────────┘
36 │
37 ▼
38┌─────────────────────────────────────────────────────────────────┐
39│ 4. TRANSITION: writer → reviewer │
40│ Execute reviewerNode(state) │
41│ → Receive update {review: "..."} │
42│ → Merge with state │
43│ Save checkpoint │
44└───────────────────────────────┬─────────────────────────────────┘
45 │
46 ▼
47┌─────────────────────────────────────────────────────────────────┐
48│ 5. CONDITIONAL TRANSITION: shouldContinue(state) │
49│ → Returns 'writer' or 'finalizer' │
50│ → If 'writer' - return to step 3 │
51│ → If 'finalizer' - proceed further │
52└───────────────────────────────┬─────────────────────────────────┘
53 │ (if 'finalizer')
54 ▼
55┌─────────────────────────────────────────────────────────────────┐
56│ 6. TRANSITION: reviewer → finalizer │
57│ Execute finalizerNode(state) │
58│ → Receive update {finalArticle: "..."} │
59│ → Merge with state │
60│ Save checkpoint │
61└───────────────────────────────┬─────────────────────────────────┘
62 │
63 ▼
64┌─────────────────────────────────────────────────────────────────┐
65│ 7. FINALIZER: finalizer → __end__ │
66│ The graph has completed │
67│ Returning the final state │
68└───────────────────────────────┬─────────────────────────────────┘
69 │
70 ▼
71 return state (full)
PART 6: FAQ - Frequently Asked Questions
Why does the reducer add messages but replace the topic?
Messages represent history. We want to keep all messages throughout the process.
Topic is the current theme. When the topic changes, the old one is no longer needed.
What happens if I don’t specify a reducer?
LangGraph will use the default behavior - replacement (like for the topic).
Why is iterationCount needed?
To protect against infinite loops. If the reviewer never says "approved", the program will keep cycling between the writer and the reviewer.
Can one node directly invoke another?
No. Nodes do not know about each other. They only read/write state. LangGraph determines who executes next based on transitions.
What are start and end?
These are special virtual nodes:
__start__ - where the graph begins
__end__ - where the graph ends
They do not execute code - they only mark the boundaries.
Can there be multiple end nodes?
Yes! For example:
1.addEdge('success', '__end__')
2.addEdge('error', '__end__')
Conclusion
Congratulations! You've learned the core concepts of LangGraph. Let's recap what we covered:
| Concept | What it does |
|---|
| State + Annotation | Defines the data structure passed between agents |
| Reducer | Specifies how to combine new data with existing data |
| Nodes (Agents) | Functions that process the state |
| Edges (Transitions) | Rules determining the order of operations |
| Conditional Edges | Dynamic selection of the next node |
| Checkpointer | Saves the state for continuation later |
What's Next?
Now that you understand the basics, you can:
- Build your own agent system - Start with a simple two-node graph and gradually add complexity
- Experiment with different flows - Try creating graphs with multiple conditional branches
- Add persistence - Use
MemorySaver or database-backed checkpointers for production
- Explore advanced features - Look into subgraphs, parallel execution, and human-in-the-loop patterns
Key Takeaways
- Think in graphs: Break down your AI workflow into discrete steps (nodes) connected by rules (edges)
- State is everything: All communication between nodes happens through the shared state
- Reducers matter: Choose the right reducer for each field - append for history, replace for current values
- Cycles enable iteration: Unlike simple chains, graphs can loop back for refinement
The article-writing example we built demonstrates a real-world pattern: research → write → review → (repeat if needed) → finalize. This same pattern applies to many AI applications: code review, content moderation, multi-step reasoning, and more.
Happy building! 🚀