What GC finds in LangGraph code
Defects found in public LangGraph code, one tab per check guardrail-calculus (GC) finds them with, each drawn as GC reads it when this site was built. Repositories are not named. Where a finding is a defect, its maintainers are told privately before any detail is published.
total: atnode_1, some run takes none ofroute_1: node_4 | node_2 | node_3.
The shape in general. A router tests the values its node can write, and one of them -- often none at all, a field the model left unset -- has no branch, so no rule takes the run.
%%{init: {"themeCSS": ".node rect, .node circle, .node ellipse, .node path, .node polygon { fill: color-mix(in srgb, var(--fg-ink) 5%, var(--fg-paper)); stroke: var(--fg-row-check-border); stroke-width: 1.5px; rx: 6px; filter: drop-shadow(0 1px 3px rgb(15 23 42 / 22%)); } .label, .label span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .label text { fill: var(--fg-ink); } .label foreignObject { line-height: normal; overflow: visible; } .edgePath .path, .flowchart-link { stroke: var(--fg-edge); } .edgePath .arrowheadPath { fill: var(--fg-edge); stroke: none; } marker { fill: var(--fg-edge) !important; } .edgeLabel, .edgeLabel p, .edgeLabel span, .labelBkg { background-color: var(--fg-paper); color: var(--fg-edge); } .edgeLabel rect { fill: var(--fg-paper); opacity: 1; } .cluster rect { fill: var(--fg-paper); stroke: var(--fg-line); stroke-width: 2px; rx: 14px; } .cluster span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .node[class*=\"verdict-\"] rect { stroke-width: 2px; rx: 999px; } .node[class*=\"verdict-\"] .nodeLabel { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .node.verdict-verified rect { stroke: var(--fg-verified); } .node.verdict-verified .nodeLabel { color: var(--fg-verified); } .node.verdict-falsified rect { stroke: var(--fg-falsified); } .node.verdict-falsified .nodeLabel { color: var(--fg-falsified); } .node.verdict-unknown rect { stroke: var(--fg-unknown); } .node.verdict-unknown .nodeLabel { color: var(--fg-unknown); } .node.verdict-inconsistent rect { stroke: var(--fg-inconsistent); } .node.verdict-inconsistent .nodeLabel { color: var(--fg-inconsistent); } .node.dead rect { stroke: var(--fg-falsified); stroke-width: 2px; } .node.dead .nodeLabel { color: var(--fg-falsified); } .node.unread rect { stroke: var(--fg-unknown); stroke-dasharray: 4 3; } .node.unread .nodeLabel { color: var(--fg-unknown); } .node.flagged rect { stroke: var(--fg-inconsistent); stroke-width: 3px; } .node.flagged .nodeLabel { color: var(--fg-inconsistent); }"}}%%
flowchart LR
w["node writes k"] --> r{"router on k"}
r -- "k = a" --> a["a"]
r -- "k = b" --> b["b"]
r -. "k unset: no rule" .-> x(["KeyError"])
What LangGraph did when it was run
- With every unread choice false: A run takes
node_1→node_4and ends with the keyskey_1,key_2. - With
node_1_return_1taken, whatever else is: A run takesnode_1, then raisesKeyError: None. - With
node_1_return_2taken and nothing else: A run takesnode_1→node_2and ends with the keyskey_1,key_2. - With
node_1_return_3taken and nothing else: A run takesnode_1→node_3and ends with the keyskey_1,key_2.
disjoint: atnode_3, some run takes both ofgoto: node_2 & node_1.
The shape in general. A node decides where to go by returning Command(goto=...), and its static edge sends the run on as well: both run, in one step.
%%{init: {"themeCSS": ".node rect, .node circle, .node ellipse, .node path, .node polygon { fill: color-mix(in srgb, var(--fg-ink) 5%, var(--fg-paper)); stroke: var(--fg-row-check-border); stroke-width: 1.5px; rx: 6px; filter: drop-shadow(0 1px 3px rgb(15 23 42 / 22%)); } .label, .label span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .label text { fill: var(--fg-ink); } .label foreignObject { line-height: normal; overflow: visible; } .edgePath .path, .flowchart-link { stroke: var(--fg-edge); } .edgePath .arrowheadPath { fill: var(--fg-edge); stroke: none; } marker { fill: var(--fg-edge) !important; } .edgeLabel, .edgeLabel p, .edgeLabel span, .labelBkg { background-color: var(--fg-paper); color: var(--fg-edge); } .edgeLabel rect { fill: var(--fg-paper); opacity: 1; } .cluster rect { fill: var(--fg-paper); stroke: var(--fg-line); stroke-width: 2px; rx: 14px; } .cluster span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .node[class*=\"verdict-\"] rect { stroke-width: 2px; rx: 999px; } .node[class*=\"verdict-\"] .nodeLabel { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .node.verdict-verified rect { stroke: var(--fg-verified); } .node.verdict-verified .nodeLabel { color: var(--fg-verified); } .node.verdict-falsified rect { stroke: var(--fg-falsified); } .node.verdict-falsified .nodeLabel { color: var(--fg-falsified); } .node.verdict-unknown rect { stroke: var(--fg-unknown); } .node.verdict-unknown .nodeLabel { color: var(--fg-unknown); } .node.verdict-inconsistent rect { stroke: var(--fg-inconsistent); } .node.verdict-inconsistent .nodeLabel { color: var(--fg-inconsistent); } .node.dead rect { stroke: var(--fg-falsified); stroke-width: 2px; } .node.dead .nodeLabel { color: var(--fg-falsified); } .node.unread rect { stroke: var(--fg-unknown); stroke-dasharray: 4 3; } .node.unread .nodeLabel { color: var(--fg-unknown); } .node.flagged rect { stroke: var(--fg-inconsistent); stroke-width: 3px; } .node.flagged .nodeLabel { color: var(--fg-inconsistent); }"}}%%
flowchart LR
n["node"] -- "goto: its decision" --> g["one node"]
n -- "static edge: always" --> e["another"]
What LangGraph did when it was run
- With every unread choice false: A run takes
node_1→ (node_3→node_1,node_2) ×12, then raisesGraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.. - With
choice_1taken, whatever else is: A run takesnode_1and ends with the keyskey_1.
never-supplied:node_2readskey_3, which no run supplies: no node stores it, and the run's input does not carry it.reachable: atnode_2, no run takesroute_1 → node_1.
The shape in general. A router branches on a key no run holds when it reads it -- one no channel stores, set by an earlier node or passed in -- so one branch never fires and another always does.
%%{init: {"themeCSS": ".node rect, .node circle, .node ellipse, .node path, .node polygon { fill: color-mix(in srgb, var(--fg-ink) 5%, var(--fg-paper)); stroke: var(--fg-row-check-border); stroke-width: 1.5px; rx: 6px; filter: drop-shadow(0 1px 3px rgb(15 23 42 / 22%)); } .label, .label span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .label text { fill: var(--fg-ink); } .label foreignObject { line-height: normal; overflow: visible; } .edgePath .path, .flowchart-link { stroke: var(--fg-edge); } .edgePath .arrowheadPath { fill: var(--fg-edge); stroke: none; } marker { fill: var(--fg-edge) !important; } .edgeLabel, .edgeLabel p, .edgeLabel span, .labelBkg { background-color: var(--fg-paper); color: var(--fg-edge); } .edgeLabel rect { fill: var(--fg-paper); opacity: 1; } .cluster rect { fill: var(--fg-paper); stroke: var(--fg-line); stroke-width: 2px; rx: 14px; } .cluster span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .node[class*=\"verdict-\"] rect { stroke-width: 2px; rx: 999px; } .node[class*=\"verdict-\"] .nodeLabel { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .node.verdict-verified rect { stroke: var(--fg-verified); } .node.verdict-verified .nodeLabel { color: var(--fg-verified); } .node.verdict-falsified rect { stroke: var(--fg-falsified); } .node.verdict-falsified .nodeLabel { color: var(--fg-falsified); } .node.verdict-unknown rect { stroke: var(--fg-unknown); } .node.verdict-unknown .nodeLabel { color: var(--fg-unknown); } .node.verdict-inconsistent rect { stroke: var(--fg-inconsistent); } .node.verdict-inconsistent .nodeLabel { color: var(--fg-inconsistent); } .node.dead rect { stroke: var(--fg-falsified); stroke-width: 2px; } .node.dead .nodeLabel { color: var(--fg-falsified); } .node.unread rect { stroke: var(--fg-unknown); stroke-dasharray: 4 3; } .node.unread .nodeLabel { color: var(--fg-unknown); } .node.flagged rect { stroke: var(--fg-inconsistent); stroke-width: 3px; } .node.flagged .nodeLabel { color: var(--fg-inconsistent); }"}}%%
flowchart LR
s["earlier node or input"] -- "k: no channel" --> x(("dropped"))
m["node"] --> r{"router reads k"}
r -. "k set: never" .-> d["intended branch"]
r -- "otherwise: always" --> o["fallback"]
What LangGraph did when it was run
- With every unread choice false: A run takes
node_2×25, then raisesGraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key..
never-supplied:node_2readskey_3, which no run supplies: no node stores it, and the run's input does not carry it.reachable: atnode_2, no run takesroute_1 → node_1.
The shape in general. A node reads a key nothing stores before it runs: its writer returned it under no channel, or the input carried it past the input schema, so the read is always empty.
%%{init: {"themeCSS": ".node rect, .node circle, .node ellipse, .node path, .node polygon { fill: color-mix(in srgb, var(--fg-ink) 5%, var(--fg-paper)); stroke: var(--fg-row-check-border); stroke-width: 1.5px; rx: 6px; filter: drop-shadow(0 1px 3px rgb(15 23 42 / 22%)); } .label, .label span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .label text { fill: var(--fg-ink); } .label foreignObject { line-height: normal; overflow: visible; } .edgePath .path, .flowchart-link { stroke: var(--fg-edge); } .edgePath .arrowheadPath { fill: var(--fg-edge); stroke: none; } marker { fill: var(--fg-edge) !important; } .edgeLabel, .edgeLabel p, .edgeLabel span, .labelBkg { background-color: var(--fg-paper); color: var(--fg-edge); } .edgeLabel rect { fill: var(--fg-paper); opacity: 1; } .cluster rect { fill: var(--fg-paper); stroke: var(--fg-line); stroke-width: 2px; rx: 14px; } .cluster span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .node[class*=\"verdict-\"] rect { stroke-width: 2px; rx: 999px; } .node[class*=\"verdict-\"] .nodeLabel { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .node.verdict-verified rect { stroke: var(--fg-verified); } .node.verdict-verified .nodeLabel { color: var(--fg-verified); } .node.verdict-falsified rect { stroke: var(--fg-falsified); } .node.verdict-falsified .nodeLabel { color: var(--fg-falsified); } .node.verdict-unknown rect { stroke: var(--fg-unknown); } .node.verdict-unknown .nodeLabel { color: var(--fg-unknown); } .node.verdict-inconsistent rect { stroke: var(--fg-inconsistent); } .node.verdict-inconsistent .nodeLabel { color: var(--fg-inconsistent); } .node.dead rect { stroke: var(--fg-falsified); stroke-width: 2px; } .node.dead .nodeLabel { color: var(--fg-falsified); } .node.unread rect { stroke: var(--fg-unknown); stroke-dasharray: 4 3; } .node.unread .nodeLabel { color: var(--fg-unknown); } .node.flagged rect { stroke: var(--fg-inconsistent); stroke-width: 3px; } .node.flagged .nodeLabel { color: var(--fg-inconsistent); }"}}%%
flowchart LR
w["writer returns k"] -- "no channel" --> x(("dropped"))
w --> n["next node"]
n -. "reads k: always empty" .-> x
What LangGraph did when it was run
- With every unread choice false: A run takes
node_2×25, then raisesGraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key..
unmerged-write: two nodes of the step afternode_2both writekey_2, which no reducer merges: LangGraph raisesInvalidUpdateError.
The shape in general. One node's static edges run two nodes in one step, and both write a key no reducer merges, so LangGraph raises InvalidUpdateError.
%%{init: {"themeCSS": ".node rect, .node circle, .node ellipse, .node path, .node polygon { fill: color-mix(in srgb, var(--fg-ink) 5%, var(--fg-paper)); stroke: var(--fg-row-check-border); stroke-width: 1.5px; rx: 6px; filter: drop-shadow(0 1px 3px rgb(15 23 42 / 22%)); } .label, .label span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .label text { fill: var(--fg-ink); } .label foreignObject { line-height: normal; overflow: visible; } .edgePath .path, .flowchart-link { stroke: var(--fg-edge); } .edgePath .arrowheadPath { fill: var(--fg-edge); stroke: none; } marker { fill: var(--fg-edge) !important; } .edgeLabel, .edgeLabel p, .edgeLabel span, .labelBkg { background-color: var(--fg-paper); color: var(--fg-edge); } .edgeLabel rect { fill: var(--fg-paper); opacity: 1; } .cluster rect { fill: var(--fg-paper); stroke: var(--fg-line); stroke-width: 2px; rx: 14px; } .cluster span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .node[class*=\"verdict-\"] rect { stroke-width: 2px; rx: 999px; } .node[class*=\"verdict-\"] .nodeLabel { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .node.verdict-verified rect { stroke: var(--fg-verified); } .node.verdict-verified .nodeLabel { color: var(--fg-verified); } .node.verdict-falsified rect { stroke: var(--fg-falsified); } .node.verdict-falsified .nodeLabel { color: var(--fg-falsified); } .node.verdict-unknown rect { stroke: var(--fg-unknown); } .node.verdict-unknown .nodeLabel { color: var(--fg-unknown); } .node.verdict-inconsistent rect { stroke: var(--fg-inconsistent); } .node.verdict-inconsistent .nodeLabel { color: var(--fg-inconsistent); } .node.dead rect { stroke: var(--fg-falsified); stroke-width: 2px; } .node.dead .nodeLabel { color: var(--fg-falsified); } .node.unread rect { stroke: var(--fg-unknown); stroke-dasharray: 4 3; } .node.unread .nodeLabel { color: var(--fg-unknown); } .node.flagged rect { stroke: var(--fg-inconsistent); stroke-width: 3px; } .node.flagged .nodeLabel { color: var(--fg-inconsistent); }"}}%%
flowchart LR
s["node"] --> a["a writes k"]
s --> b["b writes k"]
a --> m(["k: no reducer, InvalidUpdateError"])
b --> m
What LangGraph did when it was run
- With every unread choice false: A run takes
node_2→node_1,node_3and ends with the keyskey_1. - With
node_1_return_1taken and nothing else; or withnode_3_return_1taken and nothing else: A run takesnode_2→node_1,node_3and ends with the keyskey_1,key_2. - With
node_1_return_1,node_3_return_1taken, whatever else is: A run takesnode_2→node_1,node_3, then raisesInvalidUpdateError: At key 'key_2': Can receive only one value per step. Use an Annotated key to handle multiple values..
dangling-route:node_1routes to a node the graph does not have.
The shape in general. A route names a node the graph does not have: a router's return its path map lacks, or a goto to a node no longer registered.
%%{init: {"themeCSS": ".node rect, .node circle, .node ellipse, .node path, .node polygon { fill: color-mix(in srgb, var(--fg-ink) 5%, var(--fg-paper)); stroke: var(--fg-row-check-border); stroke-width: 1.5px; rx: 6px; filter: drop-shadow(0 1px 3px rgb(15 23 42 / 22%)); } .label, .label span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .label text { fill: var(--fg-ink); } .label foreignObject { line-height: normal; overflow: visible; } .edgePath .path, .flowchart-link { stroke: var(--fg-edge); } .edgePath .arrowheadPath { fill: var(--fg-edge); stroke: none; } marker { fill: var(--fg-edge) !important; } .edgeLabel, .edgeLabel p, .edgeLabel span, .labelBkg { background-color: var(--fg-paper); color: var(--fg-edge); } .edgeLabel rect { fill: var(--fg-paper); opacity: 1; } .cluster rect { fill: var(--fg-paper); stroke: var(--fg-line); stroke-width: 2px; rx: 14px; } .cluster span { color: var(--fg-ink); font-family: var(--md-mermaid-font-family); } .node[class*=\"verdict-\"] rect { stroke-width: 2px; rx: 999px; } .node[class*=\"verdict-\"] .nodeLabel { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } .node.verdict-verified rect { stroke: var(--fg-verified); } .node.verdict-verified .nodeLabel { color: var(--fg-verified); } .node.verdict-falsified rect { stroke: var(--fg-falsified); } .node.verdict-falsified .nodeLabel { color: var(--fg-falsified); } .node.verdict-unknown rect { stroke: var(--fg-unknown); } .node.verdict-unknown .nodeLabel { color: var(--fg-unknown); } .node.verdict-inconsistent rect { stroke: var(--fg-inconsistent); } .node.verdict-inconsistent .nodeLabel { color: var(--fg-inconsistent); } .node.dead rect { stroke: var(--fg-falsified); stroke-width: 2px; } .node.dead .nodeLabel { color: var(--fg-falsified); } .node.unread rect { stroke: var(--fg-unknown); stroke-dasharray: 4 3; } .node.unread .nodeLabel { color: var(--fg-unknown); } .node.flagged rect { stroke: var(--fg-inconsistent); stroke-width: 3px; } .node.flagged .nodeLabel { color: var(--fg-inconsistent); }"}}%%
flowchart LR
r{"router or goto"} -- "returns y" --> y["y"]
r -. "returns x" .-> x(["no node x: KeyError, or dropped"])
What LangGraph did when it was run
- With every unread choice false: A run takes
node_1and ends with the keyskey_1.
Reading a check's tab
GC reads a LangGraph graph without running it: which node runs after which, what each one reads from the state and writes back, and what each router tests. It then asks, of every run the graph allows, whether a read can find nothing, whether two writes collide, whether a route leads nowhere.
Each tab's example is generated, not written: a defect's shape, read by GC and printed back as a LangGraph module with every name replaced. None of it is code from a scanned repository. The canvas is that module as GC reads it: every node a vertex with its rules and routes, and what its checks found. Under it, what GC derives of the module, then the shape every defect of that check has, drawn in general.
Folded under each are what LangGraph did when the module was run, and the module itself. Where GC does not read a decision -- which return a node takes, which way a router it cannot read goes -- the module asks a choice of its own, and it was run once for every way those choices go: each outcome is listed with the fewest choices that lead to it.
The scan
| repositories scanned | modules GC read | not finished | Horn questions asked | answered alike by both engines | defects recorded | in repositories | since fixed upstream |
|---|---|---|---|---|---|---|---|
| 157 | 1038 | 0 | 10075 | 10075 | 14 | 12 | 1 |
Both of GC's engines -- one in Python, one in Rust -- answered every question, and the table counts how often they agreed.
By check
Each finding was read by a person and judged: a bug, a copy of one found elsewhere, a defect that is not a bug in practice, or not a bug -- where GC's reading was too coarse. "Found in the code itself" counts the recorded defects GC derives from the repository's own module, rather than only from a smaller graph of the same shape.
| check | finds | defects | repositories | found in the code itself | corpus findings | bug | copy | defect | not-a-bug |
|---|---|---|---|---|---|---|---|---|---|
total |
A state no rule takes | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
disjoint |
Two rules at once | 1 | 1 | 1 | 3 | 3 | 0 | 0 | 0 |
reachable |
A rule that never fires | 3 | 3 | 2 | 4 | 1 | 0 | 3 | 0 |
never-supplied |
A read nothing supplies | 6 | 6 | 6 | 35 | 14 | 4 | 0 | 17 |
unmerged-write |
A write no reducer merges | 1 | 1 | 1 | 5 | 5 | 0 | 0 | 0 |
dangling-route |
A route to nowhere | 3 | 3 | 3 | 11 | 4 | 0 | 3 | 4 |