#8950·langgraph

add_node: node protocols make the `state` parameter name load-bearing for type checkers

Author: AndreaBozzoCreated Sep 17, 2026Updated Sep 17, 2026
Labelsexternal

Node protocols declare state as positional-or-keyword, so its name is load-bearing for type checkers while the runtime passes it positionally.

Repro

mypy --strict, langgraph 1.2.11:

python
from collections.abc import Awaitable, Callable
from typing import Any, TypedDict

from langgraph.graph import StateGraph


class State(TypedDict):
    x: int


async def named_state(state: State) -> dict[str, Any]:
    return {}


async def named_data(data: State) -> dict[str, Any]:
    return {}


NodeFn = Callable[[State], Awaitable[dict[str, Any]]]


def factory() -> NodeFn:
    return named_state


def build() -> None:
    g: StateGraph[State, Any, Any, Any] = StateGraph(State)
    g.add_node("a", named_state)  # ok
    g.add_node("b", named_data)   # error: incompatible type
    g.add_node("c", factory())    # error: incompatible type

Without the explicit StateGraph[...] parametrisation the same two lines fail as no overload variant of "add_node" matches, which lists every overload and is hard to act on.

Both nodes run fine — named_data returns {'x': 42}.

Cause

libs/langgraph/langgraph/graph/_node.py:

python
class _Node(Protocol[NodeInputT_contra]):
    def __call__(self, state: NodeInputT_contra) -> Any: ...

state is not positional-only, so:

  1. a node's first parameter must be named state, although _internal/_runnable.py calls it positionally (args = (input,), then self.func(*args, **kwargs));
  2. Callable[[State], Awaitable[...]] can never satisfy the protocol, since Callable parameters are positional-only — so typing a node factory with a Callable alias always fails, whatever the function is named.

Suggested fix

Mark the parameter positional-only in the nine node protocols:

python
def __call__(self, state: NodeInputT_contra, /) -> Any: ...

This only widens what satisfies the protocol; nodes that already name it state keep matching. config, writer, store and runtime are passed as keyword arguments, so they are unaffected.

Verified locally: adding / to _Node alone makes all three lines above pass, and the change is annotation-only (126 tests in a downstream project still pass against the patched file).

Happy to open the PR if you would like it.