diff --git a/src/docs.json b/src/docs.json
index 12c82e9a6..50f1a3f3f 100644
--- a/src/docs.json
+++ b/src/docs.json
@@ -195,6 +195,7 @@
"oss/python/langgraph/install",
"oss/python/langgraph/quickstart",
"oss/python/langgraph/local-server",
+ "oss/python/langgraph/thinking-in-langgraph",
"oss/python/langgraph/workflows-agents"
]
},
@@ -1213,6 +1214,7 @@
"oss/javascript/langgraph/install",
"oss/javascript/langgraph/quickstart",
"oss/javascript/langgraph/local-server",
+ "oss/javascript/langgraph/thinking-in-langgraph",
"oss/javascript/langgraph/workflows-agents"
]
},
diff --git a/src/oss/langgraph/thinking-in-langgraph.mdx b/src/oss/langgraph/thinking-in-langgraph.mdx
new file mode 100644
index 000000000..26da787c8
--- /dev/null
+++ b/src/oss/langgraph/thinking-in-langgraph.mdx
@@ -0,0 +1,1115 @@
+---
+title: Thinking in LangGraph
+description: Learn how to think about building agents with LangGraph by breaking down a customer support email agent into discrete steps
+---
+
+LangGraph can change how you think about the agents you build. When you build an agent with LangGraph, you will first break it apart into discrete steps called **nodes**. Then, you will describe the different decisions and transitions for each of your nodes. Finally, you will connect your nodes together through a shared **state** that each node can read from and write to. In this tutorial, we'll guide you through the thought process of building a customer support email agent with LangGraph.
+
+## Start with the process you want to automate
+
+Imagine that you need to build an AI agent that handles customer support emails. Your product team has given you these requirements:
+
+The agent should:
+
+- Read incoming customer emails
+- Classify them by urgency and topic
+- Search relevant documentation to answer questions
+- Draft appropriate responses
+- Escalate complex issues to human agents
+- Schedule follow-ups when needed
+
+Example scenarios to handle:
+
+1. Simple product question: "How do I reset my password?"
+2. Bug report: "The export feature crashes when I select PDF format"
+3. Urgent billing issue: "I was charged twice for my subscription!"
+4. Feature request: "Can you add dark mode to the mobile app?"
+5. Complex technical issue: "Our API integration fails intermittently with 504 errors"
+
+To implement an agent in LangGraph, you will usually follow the same five steps.
+
+## Step 1: Map out your workflow as discrete steps
+
+Start by identifying the distinct steps in your process. Each step will become a **node** (a function that does one specific thing). Then sketch how these steps connect to each other.
+
+```mermaid
+flowchart TD
+ A[START] --> B[Read Email]
+ B --> C[Classify Intent]
+
+ C -.-> D[Doc Search]
+ C -.-> E[Bug Track]
+ C -.-> F[Human Review]
+
+ D --> G[Draft Reply]
+ E --> G
+ F --> G
+
+ G -.-> H[Human Review]
+ G -.-> I[Send Reply]
+
+ H --> J[END]
+ I --> J[END]
+```
+
+The arrows show possible paths, but the actual decision of which path to take happens inside each node.
+
+Now that you've identified the components in your workflow, let's understand what each node needs to do:
+
+- Read Email: Extract and parse the email content
+- Classify Intent: Use an LLM to categorize urgency and topic, then route to appropriate action
+- Doc Search: Query your knowledge base for relevant information
+- Bug Track: Create or update issue in tracking system
+- Draft Reply: Generate an appropriate response
+- Human Review: Escalate to human agent for approval or handling
+- Send Reply: Dispatch the email response
+
+
+Notice that some nodes make decisions about where to go next (Classify Intent, Draft Reply, Human Review), while others always proceed to the same next step (Read Email always goes to Classify Intent, Doc Search always goes to Draft Reply).
+
+
+## Step 2: Identify what each step needs to do
+
+For each node in your graph, determine what type of operation it represents and what context it needs to work properly.
+
+
+
+ Use when you need to understand, analyze, generate text, or make reasoning decisions
+
+
+ Use when you need to retrieve information from external sources
+
+
+ Use when you need to perform external actions
+
+
+ Use when you need human intervention
+
+
+
+### LLM Steps
+
+When a step needs to understand, analyze, generate text, or make reasoning decisions:
+
+
+
+ - Static context (prompt): Classification categories, urgency definitions, response format
+ - Dynamic context (from state): Email content, sender information
+ - Desired outcome: Structured classification that determines routing
+
+
+
+ - Static context (prompt): Tone guidelines, company policies, response templates
+ - Dynamic context (from state): Classification results, search results, customer history
+ - Desired outcome: Professional email response ready for review
+
+
+
+### Data Steps
+
+When a step needs to retrieve information from external sources:
+
+
+
+ - Parameters: Query built from intent and topic
+ - Retry strategy: Yes, with exponential backoff for transient failures
+ - Caching: Could cache common queries to reduce API calls
+
+
+
+ - Parameters: Customer email or ID from state
+ - Retry strategy: Yes, but with fallback to basic info if unavailable
+ - Caching: Yes, with time-to-live to balance freshness and performance
+
+
+
+### Action Steps
+
+When a step needs to perform an external action:
+
+
+
+ - When to execute: After approval (human or automated)
+ - Retry strategy: Yes, with exponential backoff for network issues
+ - Should not cache: Each send is a unique action
+
+
+
+ - When to execute: Always when intent is "bug"
+ - Retry strategy: Yes, critical to not lose bug reports
+ - Returns: Ticket ID to include in response
+
+
+
+### User Input Steps
+
+When a step needs human intervention:
+
+
+
+ - Context for decision: Original email, draft response, urgency, classification
+ - Expected input format: Approval boolean plus optional edited response
+ - When triggered: High urgency, complex issues, or quality concerns
+
+
+
+## Step 3: Design your state
+
+State is the shared [memory](/oss/concepts/memory) accessible to all nodes in your agent. Think of it as the notebook your agent uses to keep track of everything it learns and decides as it works through the process.
+
+### What belongs in state?
+
+Ask yourself these questions about each piece of data:
+
+
+
+ Does it need to persist across steps? If yes, it goes in state.
+
+
+
+ Can you derive it from other data? If yes, compute it when needed instead of storing it in state.
+
+
+
+For our email agent, we need to track:
+
+- The original email and sender info (can't reconstruct these)
+- Classification results (needed by multiple downstream nodes)
+- Search results and customer data (expensive to re-fetch)
+- The draft response (needs to persist through review)
+- Execution metadata (for debugging and recovery)
+
+### Keep state raw, format prompts on-demand
+
+
+A key principle: your state should store raw data, not formatted text. Format prompts inside nodes when you need them.
+
+
+This separation means:
+
+- Different nodes can format the same data differently for their needs
+- You can change prompt templates without modifying your state schema
+- Debugging is clearer - you see exactly what data each node received
+- Your agent can evolve without breaking existing state
+
+Let's define our state:
+
+:::python
+
+```python
+from typing import TypedDict, Literal
+
+# Define the structure for email classification
+class EmailClassification(TypedDict):
+ intent: Literal["question", "bug", "billing", "feature", "complex"]
+ urgency: Literal["low", "medium", "high", "critical"]
+ topic: str
+ summary: str
+
+class EmailAgentState(TypedDict):
+ # Raw email data
+ email_content: str
+ sender_email: str
+ email_id: str
+
+ # Classification result
+ classification: EmailClassification | None
+
+ # Raw search/API results
+ search_results: list[str] | None # List of raw document chunks
+ customer_history: dict | None # Raw customer data from CRM
+
+ # Generated content
+ draft_response: str | None
+```
+
+:::
+
+:::js
+
+```typescript
+import * as z from "zod";
+
+// Define the structure for email classification
+const EmailClassificationSchema = z.object({
+ intent: z.enum(["question", "bug", "billing", "feature", "complex"]),
+ urgency: z.enum(["low", "medium", "high", "critical"]),
+ topic: z.string(),
+ summary: z.string(),
+});
+
+const EmailAgentState = z.object({
+ // Raw email data
+ emailContent: z.string(),
+ senderEmail: z.string(),
+ emailId: z.string(),
+
+ // Classification result
+ classification: EmailClassificationSchema.optional(),
+
+ // Raw search/API results
+ searchResults: z.array(z.string()).optional(), // List of raw document chunks
+ customerHistory: z.record(z.any()).optional(), // Raw customer data from CRM
+
+ // Generated content
+ responseText: z.string().optional(),
+});
+
+type EmailAgentStateType = z.infer;
+type EmailClassificationType = z.infer;
+```
+
+:::
+
+Notice that the state contains only raw data - no prompt templates, no formatted strings, no instructions. The classification output is stored as a single dictionary, straight from the LLM.
+
+## Step 4: Build your nodes
+
+:::python
+
+Now we implement each step as a function. A node in LangGraph is just a Python function that takes the current state and returns updates to it.
+
+:::
+
+:::js
+
+Now we implement each step as a function. A node in LangGraph is just a JavaScript function that takes the current state and returns updates to it.
+
+:::
+
+### Handle errors appropriately
+
+Different errors need different handling strategies:
+
+| Error Type | Who Fixes It | Strategy | When to Use |
+|------------|--------------|----------|-------------|
+| Transient errors (network issues, rate limits) | System (automatic) | Retry policy | Temporary failures that usually resolve on retry |
+| LLM-recoverable errors (tool failures, parsing issues) | LLM | Store error in state and loop back | LLM can see the error and adjust its approach |
+| User-fixable errors (missing information, unclear instructions) | Human | Pause with `interrupt()` | Need user input to proceed |
+| Unexpected errors | Developer | Let them bubble up | Unknown issues that need debugging |
+
+
+
+ Add a retry policy to automatically retry network issues and rate limits:
+
+:::python
+
+```python
+from langgraph.types import RetryPolicy
+
+workflow.add_node(
+ "search_documentation",
+ search_documentation,
+ retry_policy=RetryPolicy(max_attempts=3, initial_interval=1.0)
+)
+```
+
+:::
+
+:::js
+
+```typescript
+import type { RetryPolicy } from "@langchain/langgraph";
+
+workflow.addNode(
+ "searchDocumentation",
+ searchDocumentation,
+ {
+ retryPolicy: { maxAttempts: 3, initialInterval: 1.0 },
+ },
+);
+```
+
+:::
+
+
+
+
+ Store the error in state and loop back so the LLM can see what went wrong and try again:
+
+:::python
+
+```python
+def execute_tool(state: State) -> Command[Literal["agent", "execute_tool"]]:
+ try:
+ result = run_tool(state['tool_call'])
+ return Command(update={"tool_result": result}, goto="agent")
+ except ToolError as e:
+ # Let the LLM see what went wrong and try again
+ return Command(
+ update={"tool_result": f"Tool error: {str(e)}"},
+ goto="agent"
+ )
+```
+
+:::
+
+:::js
+
+```typescript
+import { Command } from "@langchain/langgraph";
+
+async function executeTool(state: State) {
+ try {
+ const result = await runTool(state.toolCall);
+ return new Command({
+ update: { toolResult: result },
+ goto: "agent",
+ });
+ } catch (error) {
+ // Let the LLM see what went wrong and try again
+ return new Command({
+ update: { toolResult: `Tool error: ${error}` },
+ goto: "agent"
+ });
+ }
+}
+```
+
+:::
+
+
+
+
+ Pause and collect information from the user when needed (like account IDs, order numbers, or clarifications):
+
+:::python
+
+```python
+def lookup_customer_history(state: State) -> Command[Literal["draft_response"]]:
+ if not state.get('customer_id'):
+ user_input = interrupt({
+ "message": "Customer ID needed",
+ "request": "Please provide the customer's account ID to look up their subscription history"
+ })
+ return Command(
+ update={"customer_id": user_input['customer_id']},
+ goto="lookup_customer_history"
+ )
+ # Now proceed with the lookup
+ customer_data = fetch_customer_history(state['customer_id'])
+ return Command(update={"customer_history": customer_data}, goto="draft_response")
+```
+
+:::
+
+:::js
+
+```typescript
+import { Command, interrupt } from "@langchain/langgraph";
+
+async function lookupCustomerHistory(state: State) {
+ if (!state.customerId) {
+ const userInput = interrupt({
+ message: "Customer ID needed",
+ request: "Please provide the customer's account ID to look up their subscription history",
+ });
+ return new Command({
+ update: { customerId: userInput.customerId },
+ goto: "lookupCustomerHistory",
+ });
+ }
+ // Now proceed with the lookup
+ const customerData = await fetchCustomerHistory(state.customerId);
+ return new Command({
+ update: { customerHistory: customerData },
+ goto: "draftResponse",
+ });
+}
+```
+
+:::
+
+
+
+
+ Let them bubble up for debugging. Don't catch what you can't handle:
+
+:::python
+
+```python
+def send_reply(state: EmailAgentState):
+ try:
+ email_service.send(state["draft_response"])
+ except Exception:
+ raise # Surface unexpected errors
+```
+
+:::
+
+:::js
+
+```typescript
+async function sendReply(state: EmailAgentStateType): Promise {
+ try {
+ await emailService.send(state.responseText);
+ } catch (error) {
+ throw error; // Surface unexpected errors
+ }
+}
+```
+
+:::
+
+
+
+
+
+### Implementing our email agent nodes
+
+We'll implement each node as a simple function. Remember: nodes take state, do work, and return updates.
+
+
+
+
+:::python
+
+```python
+from typing import Literal
+from langgraph.graph import StateGraph, START, END
+from langgraph.types import interrupt, Command, RetryPolicy
+from langchain_openai import ChatOpenAI
+from langchain_core.messages import HumanMessage
+
+llm = ChatOpenAI(model="gpt-4")
+
+def read_email(state: EmailAgentState) -> dict:
+ """Extract and parse email content"""
+ # In production, this would connect to your email service
+ return {
+ "messages": [HumanMessage(content=f"Processing email: {state['email_content']}")]
+ }
+
+def classify_intent(state: EmailAgentState) -> Command[Literal["search_documentation", "human_review", "draft_response", "bug_tracking"]]:
+ """Use LLM to classify email intent and urgency, then route accordingly"""
+
+ # Create structured LLM that returns EmailClassification dict
+ structured_llm = llm.with_structured_output(EmailClassification)
+
+ # Format the prompt on-demand, not stored in state
+ classification_prompt = f"""
+ Analyze this customer email and classify it:
+
+ Email: {state['email_content']}
+ From: {state['sender_email']}
+
+ Provide classification including intent, urgency, topic, and summary.
+ """
+
+ # Get structured response directly as dict
+ classification = structured_llm.invoke(classification_prompt)
+
+ # Determine next node based on classification
+ if classification['intent'] == 'billing' or classification['urgency'] == 'critical':
+ goto = "human_review"
+ elif classification['intent'] in ['question', 'feature']:
+ goto = "search_documentation"
+ elif classification['intent'] == 'bug':
+ goto = "bug_tracking"
+ else:
+ goto = "draft_response"
+
+ # Store classification as a single dict in state
+ return Command(
+ update={"classification": classification},
+ goto=goto
+ )
+```
+
+:::
+
+:::js
+
+```typescript
+import { StateGraph, START, END, Command } from "@langchain/langgraph";
+import { HumanMessage } from "@langchain/core/messages";
+import { ChatAnthropic } from "@langchain/anthropic";
+
+const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
+
+async function readEmail(state: EmailAgentStateType) {
+ // Extract and parse email content
+ // In production, this would connect to your email service
+ console.log(`Processing email: ${state.emailContent}`);
+ return {};
+}
+
+async function classifyIntent(state: EmailAgentStateType) {
+ // Use LLM to classify email intent and urgency, then route accordingly
+
+ // Create structured LLM that returns EmailClassification object
+ const structuredLlm = llm.withStructuredOutput(EmailClassificationSchema);
+
+ // Format the prompt on-demand, not stored in state
+ const classificationPrompt = `
+ Analyze this customer email and classify it:
+
+ Email: ${state.emailContent}
+ From: ${state.senderEmail}
+
+ Provide classification including intent, urgency, topic, and summary.
+ `;
+
+ // Get structured response directly as object
+ const classification = await structuredLlm.invoke(classificationPrompt);
+
+ // Determine next node based on classification
+ let nextNode: "searchDocumentation" | "humanReview" | "draftResponse" | "bugTracking";
+
+ if (classification.intent === "billing" || classification.urgency === "critical") {
+ nextNode = "humanReview";
+ } else if (classification.intent === "question" || classification.intent === "feature") {
+ nextNode = "searchDocumentation";
+ } else if (classification.intent === "bug") {
+ nextNode = "bugTracking";
+ } else {
+ nextNode = "draftResponse";
+ }
+
+ // Store classification as a single object in state
+ return new Command({
+ update: { classification },
+ goto: nextNode,
+ });
+}
+```
+
+:::
+
+
+
+
+
+:::python
+
+```python
+def search_documentation(state: EmailAgentState) -> Command[Literal["draft_response"]]:
+ """Search knowledge base for relevant information"""
+
+ # Build search query from classification
+ classification = state.get('classification', {})
+ query = f"{classification.get('intent', '')} {classification.get('topic', '')}"
+
+ try:
+ # Implement your search logic here
+ # Store raw search results, not formatted text
+ search_results = [
+ "Reset password via Settings > Security > Change Password",
+ "Password must be at least 12 characters",
+ "Include uppercase, lowercase, numbers, and symbols"
+ ]
+ except SearchAPIError as e:
+ # For recoverable search errors, store error and continue
+ search_results = [f"Search temporarily unavailable: {str(e)}"]
+
+ return Command(
+ update={"search_results": search_results}, # Store raw results or error
+ goto="draft_response"
+ )
+
+def bug_tracking(state: EmailAgentState) -> Command[Literal["draft_response"]]:
+ """Create or update bug tracking ticket"""
+
+ # Create ticket in your bug tracking system
+ ticket_id = "BUG-12345" # Would be created via API
+
+ return Command(
+ update={
+ "search_results": [f"Bug ticket {ticket_id} created"],
+ "current_step": "bug_tracked"
+ },
+ goto="draft_response"
+ )
+```
+
+:::
+
+:::js
+
+```typescript
+async function searchDocumentation(state: EmailAgentStateType) {
+ // Search knowledge base for relevant information
+
+ // Build search query from classification
+ const classification = state.classification!;
+ const query = `${classification.intent} ${classification.topic}`;
+
+ let searchResults: string[];
+
+ try {
+ // Implement your search logic here
+ // Store raw search results, not formatted text
+ searchResults = [
+ "Reset password via Settings > Security > Change Password",
+ "Password must be at least 12 characters",
+ "Include uppercase, lowercase, numbers, and symbols",
+ ];
+ } catch (error) {
+ // For recoverable search errors, store error and continue
+ searchResults = [`Search temporarily unavailable: ${error}`];
+ }
+
+ return new Command({
+ update: { searchResults }, // Store raw results or error
+ goto: "draftResponse",
+ });
+}
+
+async function bugTracking(state: EmailAgentStateType) {
+ // Create or update bug tracking ticket
+
+ // Create ticket in your bug tracking system
+ const ticketId = "BUG-12345"; // Would be created via API
+
+ return new Command({
+ update: { searchResults: [`Bug ticket ${ticketId} created`] },
+ goto: "draftResponse",
+ });
+}
+```
+
+:::
+
+
+
+
+
+:::python
+
+```python
+def draft_response(state: EmailAgentState) -> Command[Literal["human_review", "send_reply"]]:
+ """Generate response using context and route based on quality"""
+
+ classification = state.get('classification', {})
+
+ # Format context from raw state data on-demand
+ context_sections = []
+
+ if state.get('search_results'):
+ # Format search results for the prompt
+ formatted_docs = "\n".join([f"- {doc}" for doc in state['search_results']])
+ context_sections.append(f"Relevant documentation:\n{formatted_docs}")
+
+ if state.get('customer_history'):
+ # Format customer data for the prompt
+ context_sections.append(f"Customer tier: {state['customer_history'].get('tier', 'standard')}")
+
+ # Build the prompt with formatted context
+ draft_prompt = f"""
+ Draft a response to this customer email:
+ {state['email_content']}
+
+ Email intent: {classification.get('intent', 'unknown')}
+ Urgency level: {classification.get('urgency', 'medium')}
+
+ {chr(10).join(context_sections)}
+
+ Guidelines:
+ - Be professional and helpful
+ - Address their specific concern
+ - Use the provided documentation when relevant
+ """
+
+ response = llm.invoke(draft_prompt)
+
+ # Determine if human review needed based on urgency and intent
+ needs_review = (
+ classification.get('urgency') in ['high', 'critical'] or
+ classification.get('intent') == 'complex'
+ )
+
+ # Route to appropriate next node
+ goto = "human_review" if needs_review else "send_reply"
+
+ return Command(
+ update={"draft_response": response.content}, # Store only the raw response
+ goto=goto
+ )
+
+def human_review(state: EmailAgentState) -> Command[Literal["send_reply", END]]:
+ """Pause for human review using interrupt and route based on decision"""
+
+ classification = state.get('classification', {})
+
+ # interrupt() must come first - any code before it will re-run on resume
+ human_decision = interrupt({
+ "email_id": state['email_id'],
+ "original_email": state['email_content'],
+ "draft_response": state['draft_response'],
+ "urgency": classification.get('urgency'),
+ "intent": classification.get('intent'),
+ "action": "Please review and approve/edit this response"
+ })
+
+ # Now process the human's decision
+ if human_decision.get("approved"):
+ return Command(
+ update={"draft_response": human_decision.get("edited_response", state['draft_response'])},
+ goto="send_reply"
+ )
+ else:
+ # Rejection means human will handle directly
+ return Command(update={}, goto=END)
+
+def send_reply(state: EmailAgentState) -> dict:
+ """Send the email response"""
+ # Integrate with email service
+ print(f"Sending reply: {state['draft_response'][:100]}...")
+ return {}
+```
+
+:::
+
+:::js
+
+```typescript
+import { Command, interrupt } from "@langchain/langgraph";
+
+async function draftResponse(state: EmailAgentStateType) {
+ // Generate response using context and route based on quality
+
+ const classification = state.classification!;
+
+ // Format context from raw state data on-demand
+ const contextSections: string[] = [];
+
+ if (state.searchResults) {
+ // Format search results for the prompt
+ const formattedDocs = state.searchResults.map(doc => `- ${doc}`).join("\n");
+ contextSections.push(`Relevant documentation:\n${formattedDocs}`);
+ }
+
+ if (state.customerHistory) {
+ // Format customer data for the prompt
+ contextSections.push(`Customer tier: ${state.customerHistory.tier ?? "standard"}`);
+ }
+
+ // Build the prompt with formatted context
+ const draftPrompt = `
+ Draft a response to this customer email:
+ ${state.emailContent}
+
+ Email intent: ${classification.intent}
+ Urgency level: ${classification.urgency}
+
+ ${contextSections.join("\n\n")}
+
+ Guidelines:
+ - Be professional and helpful
+ - Address their specific concern
+ - Use the provided documentation when relevant
+ `;
+
+ const response = await llm.invoke([new HumanMessage(draftPrompt)]);
+
+ // Determine if human review needed based on urgency and intent
+ const needsReview = (
+ classification.urgency === "high" ||
+ classification.urgency === "critical" ||
+ classification.intent === "complex"
+ );
+
+ // Route to appropriate next node
+ const nextNode = needsReview ? "humanReview" : "sendReply";
+
+ return new Command({
+ update: { responseText: response.content.toString() }, // Store only the raw response
+ goto: nextNode,
+ });
+}
+
+async function humanReview(state: EmailAgentStateType) {
+ // Pause for human review using interrupt and route based on decision
+ const classification = state.classification!;
+
+ // interrupt() must come first - any code before it will re-run on resume
+ const humanDecision = interrupt({
+ emailId: state.emailId,
+ originalEmail: state.emailContent,
+ draftResponse: state.responseText,
+ urgency: classification.urgency,
+ intent: classification.intent,
+ action: "Please review and approve/edit this response",
+ });
+
+ // Now process the human's decision
+ if (humanDecision.approved) {
+ return new Command({
+ update: { responseText: humanDecision.editedResponse || state.responseText },
+ goto: "sendReply",
+ });
+ } else {
+ // Rejection means human will handle directly
+ return new Command({ update: {}, goto: END });
+ }
+}
+
+async function sendReply(state: EmailAgentStateType): Promise<{}> {
+ // Send the email response
+ // Integrate with email service
+ console.log(`Sending reply: ${state.responseText!.substring(0, 100)}...`);
+ return {};
+}
+```
+
+:::
+
+
+
+
+## Step 5: Wire it together
+
+Now we connect our nodes into a working graph. Since our nodes handle their own routing decisions, we only need a few essential edges.
+
+To enable [human-in-the-loop](/oss/langgraph/add-human-in-the-loop) with `interrupt()`, we need to compile with a [checkpointer](/oss/langgraph/persistence) to save state between runs:
+
+
+
+:::python
+
+```python
+from langgraph.checkpoint.memory import MemorySaver
+from langgraph.types import RetryPolicy
+
+# Create the graph
+workflow = StateGraph(EmailAgentState)
+
+# Add nodes with appropriate error handling
+workflow.add_node("read_email", read_email)
+workflow.add_node("classify_intent", classify_intent)
+
+# Add retry policy for nodes that might have transient failures
+workflow.add_node(
+ "search_documentation",
+ search_documentation,
+ retry_policy=RetryPolicy(max_attempts=3)
+)
+workflow.add_node("bug_tracking", bug_tracking)
+workflow.add_node("draft_response", draft_response)
+workflow.add_node("human_review", human_review)
+workflow.add_node("send_reply", send_reply)
+
+# Add only the essential edges
+workflow.add_edge(START, "read_email")
+workflow.add_edge("read_email", "classify_intent")
+workflow.add_edge("send_reply", END)
+
+# Compile with checkpointer for persistence
+memory = MemorySaver()
+app = workflow.compile(checkpointer=memory)
+```
+
+:::
+
+:::js
+
+```typescript
+import { MemorySaver, RetryPolicy } from "@langchain/langgraph";
+
+// Create the graph
+const workflow = new StateGraph(EmailAgentState)
+ // Add nodes with appropriate error handling
+ .addNode("readEmail", readEmail)
+ .addNode("classifyIntent", classifyIntent)
+ // Add retry policy for nodes that might have transient failures
+ .addNode(
+ "searchDocumentation",
+ searchDocumentation,
+ { retryPolicy: { maxAttempts: 3 } },
+ )
+ .addNode("bugTracking", bugTracking)
+ .addNode("draftResponse", draftResponse)
+ .addNode("humanReview", humanReview)
+ .addNode("sendReply", sendReply)
+ // Add only the essential edges
+ .addEdge(START, "readEmail");
+ .addEdge("readEmail", "classifyIntent");
+ .addEdge("sendReply", END);
+
+// Compile with checkpointer for persistence
+const memory = new MemorySaver();
+const app = workflow.compile({ checkpointer: memory });
+```
+
+:::
+
+
+
+:::python
+
+The graph structure is minimal because routing happens inside nodes through `Command` objects. Each node declares where it can go using type hints like `Command[Literal["node1", "node2"]]`, making the flow explicit and traceable.
+
+:::
+
+:::js
+
+The graph structure is minimal because routing happens inside nodes through `Command` objects. Each node declares where it can go, making the flow explicit and traceable.
+
+:::
+
+### Try out your agent
+
+Let's run our agent with an urgent billing issue that needs human review:
+
+
+
+:::python
+
+```python
+# Test with an urgent billing issue
+initial_state = {
+ "email_content": "I was charged twice for my subscription! This is urgent!",
+ "sender_email": "customer@example.com",
+ "email_id": "email_123",
+ "messages": []
+}
+
+# Run with a thread_id for persistence
+config = {"configurable": {"thread_id": "customer_123"}}
+result = app.invoke(initial_state, config)
+# The graph will pause at human_review
+print(f"Draft ready for review: {result['draft_response'][:100]}...")
+
+# When ready, provide human input to resume
+from langgraph.types import Command
+
+human_response = Command(
+ resume={
+ "approved": True,
+ "edited_response": "We sincerely apologize for the double charge. I've initiated an immediate refund..."
+ }
+)
+
+# Resume execution
+final_result = app.invoke(human_response, config)
+print(f"Email sent successfully!")
+```
+
+:::
+
+:::js
+
+```typescript
+// Test with an urgent billing issue
+const initialState: EmailAgentStateType = {
+ emailContent: "I was charged twice for my subscription! This is urgent!",
+ senderEmail: "customer@example.com",
+ emailId: "email_123"
+};
+
+// Run with a thread_id for persistence
+const config = { configurable: { thread_id: "customer_123" } };
+const result = await app.invoke(initialState, config);
+// The graph will pause at human_review
+console.log(`Draft ready for review: ${result.responseText?.substring(0, 100)}...`);
+
+// When ready, provide human input to resume
+import { Command } from "@langchain/langgraph";
+
+const humanResponse = new Command({
+ resume: {
+ approved: true,
+ editedResponse: "We sincerely apologize for the double charge. I've initiated an immediate refund...",
+ }
+});
+
+// Resume execution
+const finalResult = await app.invoke(humanResponse, config);
+console.log("Email sent successfully!");
+```
+
+:::
+
+
+
+The graph pauses when it hits `interrupt()`, saves everything to the checkpointer, and waits. It can resume days later, picking up exactly where it left off. The thread_id ensures all state for this conversation is preserved together.
+
+## Summary and next steps
+
+### Key Insights
+
+Building this email agent has shown us the LangGraph way of thinking:
+
+
+
+ Each node does one thing well. This decomposition enables streaming progress updates, durable execution that can pause and resume, and clear debugging since you can inspect state between steps.
+
+
+
+ Store raw data, not formatted text. This lets different nodes use the same information in different ways.
+
+
+
+ They take state, do work, and return updates. When they need to make routing decisions, they specify both the state updates and the next destination.
+
+
+
+ Transient failures get retries, LLM-recoverable errors loop back with context, user-fixable problems pause for input, and unexpected errors bubble up for debugging.
+
+
+
+ The `interrupt()` function pauses execution indefinitely, saves all state, and resumes exactly where it left off when you provide input. When combined with other operations in a node, it must come first.
+
+
+
+ You define the essential connections, and your nodes handle their own routing logic. This keeps control flow explicit and traceable - you can always understand what your agent will do next by looking at the current node.
+
+
+
+### Advanced considerations
+
+
+
+This section explores the trade-offs in node granularity design. Most applications can skip this and use the patterns shown above.
+
+
+You might wonder: why not combine Read Email and Classify Intent into one node? Or why separate Doc Search from Draft Reply? The answer involves trade-offs between resilience and observability.
+
+The resilience consideration: LangGraph's [durable execution](/oss/langgraph/durable-execution) creates checkpoints at node boundaries. When a workflow resumes after an interruption or failure, it starts from the beginning of the node where execution stopped. Smaller nodes mean more frequent checkpoints, which means less work to repeat if something goes wrong. If you combine multiple operations into one large node, a failure near the end means re-executing everything from the start of that node.
+
+Why we chose this breakdown for the email agent:
+
+- Isolation of external services: Doc Search and Bug Track are separate nodes because they call external APIs. If the search service is slow or fails, we want to isolate that from the LLM calls. We can add retry policies to these specific nodes without affecting others.
+
+- Intermediate visibility: Having Classify Intent as its own node lets us inspect what the LLM decided before taking action. This is valuable for debugging and monitoring—you can see exactly when and why the agent routes to human review.
+
+- Different failure modes: LLM calls, database lookups, and email sending have different retry strategies. Separate nodes let you configure these independently.
+
+- Reusability and testing: Smaller nodes are easier to test in isolation and reuse in other workflows.
+
+A different valid approach: You could combine Read Email and Classify Intent into a single node. You'd lose the ability to inspect the raw email before classification and would repeat both operations on any failure in that node. For most applications, the observability and debugging benefits of separate nodes are worth the trade-off.
+
+Application-level concerns: The caching discussion in Step 2 (whether to cache search results) is an application-level decision, not a LangGraph framework feature. You implement caching within your node functions based on your specific requirements—LangGraph doesn't prescribe this.
+
+Controlling checkpoint behavior: You can adjust when checkpoints are written using [durability modes](/oss/langgraph/durable-execution#durability-modes). The default `"async"` mode writes checkpoints in the background for good performance while maintaining durability. Use `"exit"` mode to checkpoint only at completion (faster for long-running graphs where mid-execution recovery isn't needed), or `"sync"` mode to guarantee checkpoints are written before proceeding to the next step (useful when you need to ensure state is persisted before continuing execution).
+
+
+### Where to go from here
+
+This was an introduction to thinking about building agents with LangGraph. You can extend this foundation with:
+
+
+
+ Learn how to add tool approval before execution, batch approval, and other patterns
+
+
+
+ Create subgraphs for complex multi-step operations
+
+
+
+ Add streaming to show real-time progress to users
+
+
+
+ Add observability with LangSmith for debugging and monitoring
+
+
+
+ Integrate more tools for web search, database queries, and API calls
+
+
+
+ Implement retry logic with exponential backoff for failed operations
+
+