The Problem I Was Trying to Solve§

I was building an autonomous research assistant that needed to perform multi-step investigations, sometimes spanning hours or days. The agent had to fetch documents, summarize them, cross-reference findings, and produce a final report. Every step depended on previous outputs, but the entire workflow couldn't fit into a single LLM context window—I needed persistent state and the ability to pause and resume. Off-the-shelf frameworks like a simple chain or ReAct agent fell flat because they either lost context on long runs or required manual checkpointing. I needed something that could maintain a coherent state graph, handle branching logic, and recover gracefully from API failures or timeouts.

LangGraph, a library from the LangChain ecosystem, promised exactly that: building stateful, cyclic graphs for agent workflows. But the documentation was sparse on real-world long-horizon examples. I decided to adapt it for my research agent, aiming for a system that could plan, execute, and revise its actions over many steps without human intervention. The core challenge was designing a state object that could capture the evolving plan, partial results, and error history, while the graph could loop back for self-correction.

I used Python 3.11, LangGraph (version 0.0.25 at the time), and LangChain’s standard integrations. For the LLM backend, I relied on Claude 3 Opus via Anthropic API for its strong reasoning and long context. I also used DeepSeek for some lightweight subtasks (like formatting) because of its speed. My local development environment was Cursor IDE for rapid prototyping, and I used Perplexity to quickly look up API changes. The entire project was orchestrated with langgraph’s built-in checkpointer using SQLite for persistence between runs.

[Loading prompt card for DeepSeek Chat...]
[Loading prompt card for Claude...]
[Loading prompt card for Perplexity AI...]

Installation was straightforward: pip install langgraph langchain-anthropic langchain-community. I set up environment variables for ANTHROPIC_API_KEY. The checkpointer required setting a Checkpoint object with a storage path. I also imported langgraph.graph for state management and langgraph.prebuilt for the ToolExecutor. I used the StateGraph class as the core container.

Step-by-Step: What I Actually Did§

First, I defined a state schema using TypedDict. The state needed to hold: "plan": List[Step] (a list of steps to execute), "results": Dict[str, Any] (mapping step IDs to outputs), "current_step": Optional[str], "errors": List[str], and "final_report": str. Each step had an ID, description, tool name, and dependencies. The graph would start by generating a plan from the user’s query, then execute steps in order, with possible parallel branches. I used add_conditional_edges to decide whether to continue, retry a step, or finalize.

Second, I built nodes: plan_node (LLM to create initial steps), execute_node (calls a tool and updates results), error_handler_node (logs error and decides retry or abort), and finalize_node (generates report). The execute_node read the current step from state, picked the correct tool, ran it, and stored the output. If the tool failed, it appended to errors and set current_step to None to trigger the error handler.

Third, I compiled the graph with a Checkpointer that saved state after every node execution. This allowed me to kill the process and resume later. I also added a max_retries field per step and a global timeout. Here's the core graph construction:

from langgraph.graph import StateGraph, END
from typing import TypedDict, List, Optional, Dict, Any

class Step(TypedDict):
    id: str
    description: str
    tool: str
    dependencies: List[str]

class State(TypedDict):
    plan: List[Step]
    results: Dict[str, Any]
    current_step: Optional[str]
    errors: List[str]
    final_report: str

builder = StateGraph(State)

builder.add_node("plan", plan_node)
builder.add_node("execute", execute_node)
builder.add_node("error_handler", error_handler_node)
builder.add_node("finalize", finalize_node)

builder.set_entry_point("plan")
builder.add_edge("plan", "execute")
builder.add_conditional_edges(
    "execute",
    decide_next,  # function that returns "continue", "retry", or "finalize"
    {"continue": "execute", "retry": "error_handler", "finalize": "finalize"}
)
builder.add_conditional_edges(
    "error_handler",
    decide_after_error,  # returns "execute" or "finalize"
    {"execute": "execute", "finalize": "finalize"}
)
builder.add_edge("finalize", END)

# Setup checkpointer
from langgraph.checkpoint import Checkpointer
checkpointer = Checkpointer("path/to/checkpoints")

app = builder.compile(checkpointer=checkpointer)

Then I ran the graph with a user query: app.invoke({"plan": [], "results": {}, "current_step": None, "errors": [], "final_report": ""}, config={"configurable": {"thread_id": "research-1"}})

Code Samples / Prompts Used§

One key prompt was for the plan generation. It had to be explicit about dependencies and tool names:

You are a planning agent. Given the user's request, break it down into up to 10 sequential or parallel steps. Each step must have a unique ID, a description, a tool name (choose from: web_search, fetch_url, summarize_text, cross_reference), and a list of dependency step IDs (empty for first steps). Output as JSON list.

User request: "Research the impact of AI on job markets in 2024, including sources from McKinsey and the World Economic Forum."

For the execute node, I used a simple ToolExecutor wrapper that maps tool names to functions.

What Worked Well§

The state graph approach excelled at long-horizon planning. The conditional edges allowed the agent to retry a step (e.g., if a web search timed out) without losing previous results. The checkpointer was a game-changer: I could resume a 50-step research run after a server reboot and continue exactly where it left off. The errors list let me track failures and later decide to skip problematic steps. Parallel execution (via StateGraph’s ability to fan out) sped up independent fetches.

Claude’s reasoning was crucial—it could generate coherent plans with proper dependencies. DeepSeek handled formatting tasks cheaply. Cursor’s inline diffs helped me iterate on the graph quickly. Perplexity resolved a few API quirks around checkpointing.

What Failed and Why§

Early versions had a brittle state schema: I didn't account for step ordering after retries. If a step failed and retried, its dependencies might have been overwritten. I fixed that by making results append-only with timestamps. Another failure: the decide_next function initially only checked if all steps were done, but didn't handle cycles for continuous improvement. I added a max loop count to avoid infinite loops. The biggest failure was forgetting to handle tool exceptions—the graph would crash instead of routing to error_handler. Wrapping tool calls in try-except within the execute node fixed that.

Results and Takeaways§

The final agent could autonomously execute a 30-step research plan in about 15 minutes (with API delays). It completed 95% of steps without human intervention; the remaining 5% were network errors that skipped. The checkpointer reduced rerun costs by 80% because we never recomputed successful steps. I successfully deployed this as a back-end service for internal reports.

Key Takeaways:

  • Stateful graphs with persistent checkpoints are essential for long-running agent workflows.
  • Designing a robust state schema that tracks errors and execution order is critical for self-correction.
  • Conditional edges enable flexibility (retry, skip, abort) without sacrificing control flow.
  • LangGraph's checkpointing and modular node design make it production-ready for multi-hour agent sessions.

Try It Yourself§

You can replicate this setup with the code provided. Start by defining your own state schema tailored to your domain (e.g., add a "notes" field). Experiment with different LLMs: Claude for planning, GPT-4 for execution, or local models via Ollama. Use the Checkpointer with a persistent database (SQLite works for single-user, but consider Postgres for multi-user). Run the example below with your own query and observe how the graph handles failures.

To get hands-on, clone the LangGraph quickstart and modify the state to include a plan list. Then add your own tools. The key is to iterate on the condition logic—what happens when a step fails three times? You might want to abort the entire plan or skip that step. I encourage you to push the limits: try a 50-step plan and see how your LLM and APIs hold up. The checkpointer will be your safety net.