Most Claude integrations follow the same pattern: user sends message, LLM responds, done. That works until your agent needs to:
- Pause mid-task and wait for human approval
- Branch into parallel sub-tasks
- Recover from a failed tool call without restarting
- Remember what it decided 12 steps ago
This is where LangGraph comes in. It's a state machine framework for LLM agents — and it solves a genuinely hard problem.
The Problem With Linear Agent Chains
Consider a research agent that:
- Searches the web
- Reads 5 articles
- Synthesizes a report
- Asks a human to verify before publishing
With a simple loop, if step 3 fails partway through, you start over. If the human says "redo step 2 with different search terms", you have no clean way to re-enter at step 2.
LangGraph models your agent as a directed graph where:
- Nodes = functions that transform state
- Edges = conditional logic that decides what runs next
- State = a typed object that persists across the entire run
Basic Setup (TypeScript)
npm install @langchain/langgraph @langchain/anthropic
import { StateGraph, END } from '@langchain/langgraph';
import { Annotation } from '@langchain/langgraph';
// Define your agent's state
const AgentState = Annotation.Root({
query: Annotation<string>,
searchResults: Annotation<string[]>({
reducer: (existing, update) => [...existing, ...update],
default: () => [],
}),
report: Annotation<string | null>({
default: () => null,
}),
humanApproved: Annotation<boolean>({
default: () => false,
}),
messages: Annotation<BaseMessage[]>({
reducer: (existing, update) => [...existing, ...update],
default: () => [],
}),
});
Building the Graph
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
async function searchNode(state: typeof AgentState.State) {
// Your search logic
const results = await webSearch(state.query);
return { searchResults: results };
}
async function synthesizeNode(state: typeof AgentState.State) {
const response = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 2000,
messages: [{
role: 'user',
content: `Synthesize these search results into a report:\n\n${state.searchResults.join('\n\n')}`
}]
});
return {
report: response.content[0].type === 'text' ? response.content[0].text : null
};
}
async function humanReviewNode(state: typeof AgentState.State) {
// In production, this would send an email/Slack message and pause
// For demo: auto-approve
console.log('Report ready for review:', state.report?.slice(0, 200));
return { humanApproved: true };
}
// Conditional edge: should we publish or request revision?
function shouldPublish(state: typeof AgentState.State): 'publish' | 'revise' {
if (state.humanApproved) return 'publish';
return 'revise';
}
async function publishNode(state: typeof AgentState.State) {
console.log('Publishing:', state.report);
return {};
}
// Build the graph
const graph = new StateGraph(AgentState)
.addNode('search', searchNode)
.addNode('synthesize', synthesizeNode)
.addNode('human_review', humanReviewNode)
.addNode('publish', publishNode)
.addEdge('__start__', 'search')
.addEdge('search', 'synthesize')
.addEdge('synthesize', 'human_review')
.addConditionalEdges(
'human_review',
shouldPublish,
{ publish: 'publish', revise: 'search' } // loop back on revise
)
.addEdge('publish', '__end__');
const app = graph.compile();
Running the Agent
const result = await app.invoke({
query: 'Latest AI agent frameworks 2026',
searchResults: [],
report: null,
humanApproved: false,
messages: [],
});
console.log(result.report);
Human-in-the-Loop Interrupts
This is where LangGraph gets powerful. You can pause execution at any node and resume later:
import { MemorySaver } from '@langchain/langgraph';
const checkpointer = new MemorySaver();
// Compile with checkpointing + interrupt before human_review
const appWithHuman = graph.compile({
checkpointer,
interruptBefore: ['human_review'],
});
const threadId = 'run-' + Date.now();
// First invocation: runs until human_review, then pauses
const firstRun = await appWithHuman.invoke(
{ query: 'Latest AI agent frameworks 2026' },
{ configurable: { thread_id: threadId } }
);
console.log('Paused for review. Report so far:', firstRun.report?.slice(0, 100));
// Later — after human reviews and approves:
const finalResult = await appWithHuman.invoke(
{ humanApproved: true }, // inject approval into state
{ configurable: { thread_id: threadId } } // resume same thread
);
The agent picks up exactly where it left off. State is persisted between invocations.
Parallel Sub-Agents
For complex research tasks, run sub-tasks in parallel:
async function parallelResearchNode(state: typeof AgentState.State) {
// Fan out to multiple Claude instances in parallel
const [techResults, marketResults, competitorResults] = await Promise.all([
searchNode({ ...state, query: state.query + ' technical implementation' }),
searchNode({ ...state, query: state.query + ' market analysis' }),
searchNode({ ...state, query: state.query + ' competitor landscape' }),
]);
return {
searchResults: [
...techResults.searchResults,
...marketResults.searchResults,
...competitorResults.searchResults,
]
};
}
When to Use LangGraph vs. a Simple Loop
Use LangGraph when:
- Your agent has conditional branching (different paths based on output)
- You need human-in-the-loop at specific steps
- You need to retry specific steps without restarting
- Your agent runs long enough that state persistence matters
- Multiple sub-agents need to coordinate through shared state
Skip LangGraph when:
- Single-shot queries (question → answer)
- Simple tool-use pipelines with no branching
- You control the full loop yourself and don't need the overhead
Production Considerations
Persistence:
MemorySaveris in-memory only. For production, usePostgresSaverorSqliteSaverfrom@langchain/langgraph-checkpoint-postgresObservability: LangGraph integrates with LangSmith for tracing — every node execution is logged with inputs/outputs
Error handling: Use
RetryPolicyon nodes that call external APIs:
graph.addNode('search', searchNode, {
retryPolicy: { maxAttempts: 3, initialInterval: 1000 }
});
- Streaming: Stream intermediate state updates to your UI:
for await (const chunk of await app.stream(inputs)) {
console.log('State update:', chunk);
}
The Honest Tradeoff
LangGraph adds complexity. For a simple Q&A bot, it's overkill. But for production AI applications that need auditability, human oversight, and recovery from partial failures — the state machine model is the right abstraction.
The alternative is reimplementing all of this yourself with ad-hoc state objects and manual retry logic. Most teams underestimate how quickly that becomes unmaintainable.
Building a production AI agent? The AI SaaS Starter Kit at whoffagents.com includes agent scaffolding, tool definitions, and a structured approach to human-in-the-loop workflows — without the boilerplate.
Top comments (0)