AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Debug Langgraph Agent

skill-kirill-sviridov-agent-dev-skills-debug-langgraph-agent · by kirill-sviridov

A structured checklist for debugging a broken LangGraph agent — the order in which to inspect state, reducers, edges, tools, checkpointer, and prompts. Use when a hand-written or generated agent returns the wrong result, hangs, loses messages, or crashes with an unclear error.

No reviews yet
0 installs
36 views
0.0% view→install

Install

$ agentstack add skill-kirill-sviridov-agent-dev-skills-debug-langgraph-agent

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kirill-sviridov-agent-dev-skills-debug-langgraph-agent)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Debug Langgraph Agent? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Debug LangGraph Agent

> The LangGraph-specific counterpart to superpowers:systematic-debugging, tuned to LangGraph symptoms. The goal is not to guess but to walk a checklist in the right order and localize the cause.

When to use

  • Generated LangGraph code crashes with an error.
  • The agent "works" but returns the wrong thing (loses context, loops, branches incorrectly).
  • The symptom is unclear ("it just doesn't work").
  • Before calling an external code-debugging MCP tool — first narrow the symptom down to one of the categories below.

When NOT to use

  • It's not LangGraph code (a flat LLM call / create_agent without a graph — those have different ailments).
  • The error is clearly in the code around the agent (FastAPI handler, DB query) — that's ordinary debugging, not a LangGraph specific.

§0. Look at the trace first

Before guessing from the code or classifying the symptom, look at what actually happened:

  1. LangSmith: LANGSMITH_TRACING=true (+ LANGSMITH_API_KEY) — every run is visible end to end: which nodes executed, with what state on input/output, which tool calls the model made, the full prompts. Half the sections below are resolved by a single glance at the trace.
  2. langgraph dev → Studio: a visual run of the graph + time-travel — rewind to the checkpoint before the failing node, edit state by hand, fork the execution, and see whether the bug disappears.
  3. Without a UI: graph.get_state_history(config) — programmatic access to the same checkpoints: where it stopped, what was in state at each step.

If the trace is unavailable (no key, prod without LangSmith) — walk the checklist below.

Algorithm: classify the symptom → section

| Symptom | Where to look | |---------|---------------| | Messages "get lost" between nodes | §1 State reducer | | The graph doesn't take the expected branch | §2 Routing | | A tool "isn't called" / the wrong one is | §3 Tool wiring | | Hangs on one node / infinite loop | §4 Recursion limit + edges | | KeyError / TypeError in state | §5 State schema | | HITL doesn't return control after resume | §6 interrupt/checkpointer | | The LLM returns "garbage" (not JSON, extra text) | §7 Prompt + structured output | | A strange error about async / connection closed | §8 Async + checkpointer plumbing | | Broken after deploy, fine locally | §9 Environment |

§1. State reducer (messages / artifacts get lost)

A classic symptom in generated LangGraph code. Checks:

  1. Annotated[list[AnyMessage], add_messages]add_messages is a function, not a string. Grep: Annotated.*["']add_messages["'] → if it matches, the string "reducer" is silently ignored and messages are overwritten.
  2. Append lists (artifacts, results, sub_outputs): must have a reducer. Annotated[list[X], operator.add] or a custom one. A bare list[X] with no reducer = overwrite: each node clobbers what previous ones accumulated.
  3. Scalar (label, status): no reducer, overwritten — that's the norm.
  4. Dict-state: if keys go missing — check whether the node returns {"key": value} instead of merging. Full replacement is occasionally a needed feature; usually it's a bug.

Test: add a print at the start and end of each node, see what comes in / goes out. Then compare.

§2. Routing (the graph goes the wrong way)

  1. Command(goto=...) mixed with add_conditional_edges on the same node — two routing mechanisms conflict, behavior is unpredictable. Pick one.
  2. add_conditional_edges without an explicit mapping — the third argument (the dict mapping) matters. Without it LangGraph tries to match the return value against node names literally, and "yes" doesn't find "approve".
  3. The conditional routing function returns None or a string that isn't in the edges → silent fall-through to default.

Test: graph.get_graph().draw_mermaid() — visually check where the arrows go.

§3. Tool wiring (a tool isn't called)

  1. The tool isn't passed to create_agent(tools=[...]) or to ToolNode([...]) — obvious, but common.
  2. The tool docstring doesn't explain when to call it — the model doesn't understand the trigger. Docs should read like "Use this when the user asks about X."
  3. The tool signature uses complex types (e.g. Optional[Union[X, Y]]) — the Anthropic/OpenAI schema may fail to generate. Simplify.
  4. The tool raises an exception — a ToolMessage with the error lands in state, and the LLM can loop. Wrap the tool in try/except and return a human-readable error message as a normal return.

Test: after the first LLM call, print tool_calls on the AIMessage.

§4. Recursion / loops

  1. recursion_limit defaults to 25. For long ReAct chains that's too low. Raise it: app.invoke(..., {"recursion_limit": 50}).
  2. An infinite loop tool → llm → the same tool — usually the prompt doesn't explain when to stop. Add to the system prompt: "when all the information is gathered — answer the user directly without calling tools."
  3. A conditional edge always returns the same value — a bug in the routing function.
  4. A node genuinely hangs (slow tool, stuck HTTP) — with LangGraph 1.2+ set a timeout: add_node(..., timeout=60) or timeout=TimeoutPolicy(run_timeout=120, idle_timeout=30). On expiry a NodeTimeoutError is raised (retryable by default). Works only for async nodes — a sync node with timeout= is rejected at compile time.

§5. State schema mismatch

  1. KeyError when reading state in a node — the field isn't declared in the TypedDict.
  2. invoke({...}) with extra keys — keys outside the schema are silently dropped or break nodes. Runtime metadata (userid, threadid) → config["configurable"], not state.
  3. Pydantic state vs TypedDict — both are fine, but you can't mix them in one graph.

§6. interrupt / checkpointer / resume

  1. interrupt() without a checkpointer — interrupt requires the graph to be compiled with a checkpointer. Without one it raises a RuntimeError.
  2. Command(resume=value) without a thread_id — resume goes to a foreign/new thread, and the original one stays suspended.
  3. You changed the state schema but the old checkpoint remains — deserialization breaks. Clear the checkpoints or migrate.
  4. durability='exit' + interrupt — the checkpoint is written only on exit from the graph, so an interrupt inside the graph isn't persisted and HITL silently breaks (there's nothing to resume). For HITL use durability='sync' or 'async' (the default).

§7. Prompt / structured output

  1. The LLM wraps output in markdown fences — JSON arrives inside ``` `json ... ` ``` and the parser fails. Either forbid it explicitly in the prompt, or strip defensively.
  2. response_format= with a string / made-up type — valid options: a Pydantic class directly (response_format=MySchema — auto-selects ProviderStrategy if the model supports native structured output; often the best choice) or an explicit ToolStrategy(MySchema) for models without native support. Strings like "json" don't work.
  3. A Pydantic schema with complex Union / Discriminator — models generate these poorly. Simplify.

§8. Async / checkpointer plumbing

  1. AsyncPostgresSaver must live inside a long-lived context manager (async with in the startup lifespan). If it's opened inside a request handler, the connection closes before resume, and resuming a day later breaks.
  2. asyncio.run(...) inside a nested loop (e.g. in FastAPI) — RuntimeError: This event loop is already running. Use await directly.
  3. Mixing sync .invoke() and async .ainvoke() in one graph — may work, but confuses interrupt handling.

§9. Environment

  1. Works locally, not in Docker — environment variables (OPENAI_API_KEY, OPENAI_BASE_URL) aren't passed into the container.
  2. CPU-only server vs local GPU — if an embedding model is used locally (BGE/MiniLM), everything works on CPU but slowly — a timeout kills the nodes.
  3. DNS / firewall — the LLM provider or proxy is unreachable from the container. curl from inside the container → the check.

If nothing helped

  1. Extract a minimal reproducible slice (one graph, no FastAPI/DB scaffolding).
  2. Call an external code-debugging MCP tool if you have one (optional — this assumes a local LangGraph-debugging MCP tool; without it, keep working the checklist and the minimal repro by hand).
  3. search_agent_knowledge("") (optional — assumes a local knowledge-retrieval MCP tool; skip if unavailable) — the RAG store may already have a card with the fix.
  4. If the symptom is about a feature from a recent 1.x release (see the changelog on docs.langchain.com) that isn't in RAG — search the web (and add it to your knowledge store if you keep one).

Debugging anti-patterns

  • Immediately rewriting "from scratch, differently" without localizing the cause. You waste time, learn nothing, and the same bug arrives in a new wrapper.
  • Testing with a real LLM on every iteration. Expensive, slow, non-deterministic. For state/edge logic use GenericFakeChatModel / FakeMessagesListChatModel or mocked responses.
  • Ignoring LangGraph warnings about deprecated APIs — that's often where your bug is (set_entry_point still works, but conditional_edges around it behave oddly).

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.