Skip to main content

langgraph-lint

Find LangGraph wiring bugs before they become a runtime KeyError.

LangGraph validates some things at compile() time — an edge to a node that doesn't exist, for instance, fails immediately and loudly. But a lot of real wiring bugs compile perfectly fine and only surface the first time a specific input takes a specific untested branch, weeks later, in production.

$ langgraph-lint myapp.graph:app

  [HIGH] LG003 (route_after_search): router can return 'maybe', which has no
  matching entry in the path_map (['no', 'yes']) -- this raises KeyError at
  runtime the first time this branch is taken

  [HIGH] LG001 (validate_output): 'validate_output' has no incoming edge and
  no path from START; it can never run

2 finding(s) in myapp.graph:app.

That first finding is real, reproducible, and proven — not a guess:

def router(state) -> Literal["yes", "no", "maybe"]:
    ...

graph.add_conditional_edges("a", router, {"yes": "b", "no": "c"})  # no "maybe"!

compiled = graph.compile()      # succeeds. no warning. nothing.
compiled.invoke({"x": 2})       # fine
compiled.invoke({"x": 3})       # fine
compiled.invoke({"x": 101})     # KeyError: 'maybe'  <- only now

langgraph-lint catches this the moment you write it, by checking the router's own Literal[...] return type against the path_map you gave add_conditional_edges. No invocation needed.

Install

pip install langgraph-lint                # zero dependencies
pip install "langgraph-lint[agent]"       # + a LangChain/LangGraph agent tool

Use it as a CLI

langgraph-lint myapp.graph:app             # module.path:attribute, like uvicorn
langgraph-lint myapp.graph:app --min-severity high
langgraph-lint myapp.graph:app --format json
langgraph-lint myapp.graph:app --format github   # Actions annotations

target accepts a compiled graph, an uncompiled StateGraph builder, or any object with .get_graph() — so it also works on plain LangChain LCEL runnables, just with a smaller rule set (the conditional-branch check is LangGraph-specific). Exit code 1 on findings, 0 clean, 2 on error.

Use it as a library

from langgraph_lint import lint

for finding in lint(app):
    print(finding)

Use it in an agent

from langgraph_lint.integrations import build_graph

auditor = build_graph("anthropic:claude-sonnet-4-5")
auditor.invoke({
    "messages": [{"role": "user", "content": "Check myapp.graph:app for wiring bugs."}]
})

Or get_tools() to bind check_langgraph_graph to your own agent.

Rules

Code Catches LangGraph catches it today?
LG000 Graph fails to compile() at all Raises, but as a stack trace, not a lint finding
LG001 A node with no path from START (dead code) No
LG003 A router's Literal[...] return type has a value missing from its path_map No — silent until that branch runs
LG005 A path_map branch the router's own return type can never produce (dead routing code, the mirror image of LG003) No

Two rules that seemed obviously useful up front — "node with no path to END" and "duplicate edge" — got cut before release. Both turned out to be structurally undetectable: LangGraph auto-inserts an implicit escape-hatch edge to END on essentially every node (even inside a genuine, unbreakable cycle — verified), and plain edges are deduplicated into a set before get_graph() ever sees them. A rule that can never fire is worse than no rule; better to ship four honest ones than six decorative ones.

On false positives

Every rule here has a real counter-pattern, and this tool knows about the big one: Send()-based map-reduce fan-out creates zero static edges to its target node. A node reached only via Send("worker", ...) has no incoming edge in the graph's structure at all — indistinguishable, by structure alone, from a typo'd dead node. langgraph-lint scans node and router source for Send("literal_name", ...) calls and downgrades those findings to informational rather than screaming at correct, idiomatic code.

Similarly:

  • LG005's check on unreachable path_map branches only trusts a Literal[...] type annotation, never the AST heuristic, since a false claim of "this can never happen" is worse than staying silent.
  • LG003's AST-based fallback (used when a router has no Literal[...] annotation) is a heuristic, not a guarantee — it scans return "..." statements and can miss dynamically computed values. Those findings are MEDIUM, not HIGH, and say so explicitly.

Every Finding carries a caveat field for exactly this reason. LG003 findings backed by an actual type annotation carry an empty caveat, because there's nothing to hedge — that's about as close to certain as static analysis gets.

Compatibility

Built and tested against langgraph==1.2.11. The LG003/LG005 branch checks read StateGraph.branches and BranchSpec.path/.ends -- internal, non-public attributes, not a documented API. They've been stable across recent LangGraph releases, but a future LangGraph refactor could rename them; if that happens, langgraph-lint degrades to LG001-only (dead-node detection, which only needs the public get_graph()), not a crash -- worth knowing if you pin an unusually old or new LangGraph version.

Development

uv venv && uv pip install -e ".[dev]"
pytest
ruff check . && mypy src

The test suite builds real langgraph.StateGraph objects rather than mocks. The flagship test doesn't just assert a finding fires — it then actually invokes the compiled graph and confirms the predicted KeyError really happens, so the rule's claim is proven, not just plausible.

License

MIT

Metadata

Release files for langgraph-lint 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for langgraph-lint 0.1.0
File Size Uploaded
langgraph_lint-0.1.0.tar.gz 17.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for langgraph-lint 0.1.0
File Interpreter ABI Platform
langgraph_lint-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 31.0 kB

Release files / langgraph_lint-0.1.0.tar.gz

Download URL langgraph_lint-0.1.0.tar.gz
Size 17.3 kB
Tags Source
SHA-256 checksum
How to use checksums
230d0a6ccae9660e3a9fa821d6458471e2fc41e2c7c87bb9092e07b72940365c
BLAKE2b-256 checksum
How to use checksums
943f699bdf4823bfc27c84c97529813e2389670e76caa55a8385ca65c305258a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / langgraph_lint-0.1.0-py3-none-any.whl

Download URL langgraph_lint-0.1.0-py3-none-any.whl
Size 13.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
394e1630600a1fe77335d6e58216987c7fb582c747c4e2f0a79419af807b9c60
BLAKE2b-256 checksum
How to use checksums
0ff17327981a30b785defd67b03b5cf9b78bdd73e93b2b5afaeae6afdbcf294c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page