Skip to content

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: at node_1, some run takes none of route_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_4 and ends with the keys key_1, key_2.
  • With node_1_return_1 taken, whatever else is: A run takes node_1, then raises KeyError: None.
  • With node_1_return_2 taken and nothing else: A run takes node_1 → node_2 and ends with the keys key_1, key_2.
  • With node_1_return_3 taken and nothing else: A run takes node_1 → node_3 and ends with the keys key_1, key_2.
  • disjoint: at node_3, some run takes both of goto: 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 raises GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key..
  • With choice_1 taken, whatever else is: A run takes node_1 and ends with the keys key_1.
  • never-supplied: node_2 reads key_3, which no run supplies: no node stores it, and the run's input does not carry it.
  • reachable: at node_2, no run takes route_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 raises GraphRecursionError: 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_2 reads key_3, which no run supplies: no node stores it, and the run's input does not carry it.
  • reachable: at node_2, no run takes route_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 raises GraphRecursionError: 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 after node_2 both write key_2, which no reducer merges: LangGraph raises InvalidUpdateError.

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_3 and ends with the keys key_1.
  • With node_1_return_1 taken and nothing else; or with node_3_return_1 taken and nothing else: A run takes node_2 → node_1, node_3 and ends with the keys key_1, key_2.
  • With node_1_return_1, node_3_return_1 taken, whatever else is: A run takes node_2 → node_1, node_3, then raises InvalidUpdateError: At key 'key_2': Can receive only one value per step. Use an Annotated key to handle multiple values..
  • dangling-route: node_1 routes 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_1 and ends with the keys key_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