Skip to content

add_node / add_edge / add_conditional_edges: filling in the blueprint

源码版本1.2.9

Responsibilities

StateGraph exposes three core registration methods: add_node wraps a function / Runnable as a node (add_node:662), add_edge adds a deterministic edge (add_edge:915), and add_conditional_edges adds a branching condition (add_conditional_edges:969). All three return Self, so they can be chained: builder.add_node("a", a).add_node("b", b).add_edge("a", "b").add_conditional_edges("b", route). They only append to builder.nodes / builder.edges / builder.branches; the actual translation into PregelNode + channel subscriptions happens at compile() time via attach_node / attach_edge / attach_branch.

They sit at the outermost edge of the user API: nearly every line you write when building a graph calls one of these three methods. add_node is the most complex — it infers input_schema from the function signature, infers destinations from a Literal[...] return type, and wraps the error_handler as a separate __error_handler__{node} node (error handler injection:856-870). add_edge is straightforward: it goes either into the edges set or into the waiting_edges set (multi-start edge, waits for all starts to complete). add_conditional_edges is the thinnest: it wraps path as a Runnable, then hands it to BranchSpec.from_path (from_path:89) to compute the ends dict, stored in branches[source].

Design motivation

Why does add_node infer so much from the function signature?

  • Less boilerplate: when input_schema is not passed explicitly, it is inferred from the type annotation of the first parameter (inferred input schema:815-825) — so def my_node(state: MyState): ... automatically uses MyState. When the return type is Literal["a", "b", "__end__"] it is automatically treated as destinations (Literal destinations:840-846), no need to write destinations=....
  • Node name can be omitted: the node parameter can be either a string or the function itself; a function object uses __name__ as the node name (node name inference:768-773) — a Runnable uses get_name().
  • error_handler is a separate node (error_handler node:857-870) — it is not a field on the spec, but a regular node named __error_handler__{node} is generated, and a pointer is recorded in the error_handler_node field. This way the handler is itself a Pregel node and enjoys all node capabilities: retry, metadata, tracing.
  • Unified multi-in-edge semantics: add_edge(["a", "b"], "c") does not mean "when a finishes run c, and when b finishes also run c"; it means "run c only after both a and b finish" (multi-start semantics:917-921), stored in the waiting_edges set and translated at compile time into a NamedBarrierValue channel.
  • Conditional edges can omit path_map: when path_map is not passed, it is inferred from the return type Literal["a", "b"] of the path function (Literal infers path_map:103-115) — so def route(state) -> Literal["a", "b"]: ... just works. When neither is provided, graph visualization assumes the branch may jump to any node (warning:994-997).
  • Same-name branches on the same node raise (branch duplicate name:1009-1012) — a node may have multiple conditional edges, but each one's condition name must be unique, because branches[source] is a dict keyed by name.

Key files

  • add_node signature:662-676 — accepts node / action / defer / metadata / input_schema / retry_policy / cache_policy / error_handler / destinations / timeout.
  • node name inference:768-790 — string used directly; function uses __name__; Runnable uses get_name(). Reserved names START / END / NS_SEP / NS_END raise.
  • signature type inference:803-848 — infers inferred_input_schema and Command[Literal[...]] destinations from the type hints of __call__.
  • error_handler injection:856-870 — generates the __error_handler__{node} standalone node stored in self.nodes, with field is_error_handler=True.
  • store StateNodeSpec:872-907 — stores the spec into self.nodes under one of three tiers: input_schema / inferred_input_schema / self.state_schema.
  • add_edge:915-967 — single start goes into the edges set, multi-start goes into the waiting_edges set; the start cannot be END and the end cannot be START.
  • add_conditional_edges:969-1017 — wraps path as a Runnable, BranchSpec.from_path computes ends, stored in self.branches[source][name].
  • BranchSpec.from_path:83-120 — handles three forms of path_map: dict / list / inferred from the return type Literal when neither is given; also infers input_schema.
  • add_sequence:1019-1044 — registers a series of nodes in one call (syntactic sugar; internally still loops add_node).
  • attach_edge:1537-1561 — compile time: single-start edges add a ChannelWrite to the start node's writers pointing at the end channel; multi-start edges register a NamedBarrierValue channel as the join.
  • attach_branch:1563-1596 — compile time: translates BranchSpec into ChannelWrite.register_writer, deciding which target channel to write based on ends.

Data flow

The real work in add_node happens in two sections — inferring the input schema and the destinations in the return type, then storing the spec into self.nodes:

python
if (
    isfunction(action)
    or ismethod(action)
    or ismethod(getattr(action, "__call__", None))
) and (
    hints := get_type_hints(getattr(action, "__call__"))
    or get_type_hints(action)
):
    if input_schema is None:
        first_parameter_name = next(
            iter(inspect.signature(cast(FunctionType, action)).parameters.keys())
        )
        if input_hint := hints.get(first_parameter_name):
            if isinstance(input_hint, type) and get_type_hints(input_hint):
                inferred_input_schema = input_hint
    if rtn := hints.get("return"):
        rtn_origin = get_origin(rtn)
        if rtn_origin is Union:
            rtn_args = get_args(rtn)
            for arg in rtn_args:
                arg_origin = get_origin(arg)
                if arg_origin is Command:
                    rtn = arg
                    rtn_origin = arg_origin
                    break
        if (
            rtn_origin is Command
            and (rargs := get_args(rtn))
            and get_origin(rargs[0]) is Literal
            and (vals := get_args(rargs[0]))
        ):
            ends = vals

(type inference:806-846)

The spec is then stored in the self.nodes dict under one of three tiers based on the inference result — explicit input_schema / inferred inferred_input_schema / fallback to self.state_schema:

python
if input_schema is not None:
    self.nodes[node] = StateNodeSpec[NodeInputT, ContextT](
        coerce_to_runnable(action, name=node, trace=False),
        metadata,
        input_schema=input_schema,
        retry_policy=retry_policy,
        cache_policy=cache_policy,
        error_handler_node=handler_node_name,
        ends=ends,
        defer=defer,
        timeout=timeout,
    )

(StateNodeSpec:872-883)

add_conditional_edges is very thin — the core is wrapping path as a Runnable and letting BranchSpec.from_path compute the ends dict:

python
path = coerce_to_runnable(path, name=None, trace=True)
name = path.name or "condition"
if name in self.branches[source]:
    raise ValueError(
        f"Branch with name `{path.name}` already exists for node `{source}`"
    )
self.branches[source][name] = BranchSpec.from_path(path, path_map, True)
if schema := self.branches[source][name].input_schema:
    self._add_schema(schema)
return self

(add_conditional_edges implementation:1005-1017)

Boundaries and failure modes

  • Duplicate node names raise directly (duplicate name:792-793) — Node already present; overrides are not allowed.
  • Reserved node names (reserved:794-801) — START / END / NS_SEP (|) / NS_END (:) cannot be used; the latter two would break checkpoint_ns concatenation.
  • add_edge with start END raises (END cannot be start:939-940); likewise START cannot be the end (START cannot be end:941-942) — these two constants are the graph's entry and exit and cannot be reversed.
  • Multi-in-edges require all starts to be add_noded first (multi-in-edge validation:956-964) — otherwise attach_edge at compile time cannot find the start node's writers.
  • Same-name branches on the same node raise (branch duplicate name:1009-1012), but multiple differently-named branches can coexist — this is the foundation of conditional + deterministic mixed routing.
  • add_node on an already-compiled graph only warns (compiled warning:778-782); changes are not reflected in the already-compiled instance — a typical ordering pitfall.

Summary

add_node / add_edge / add_conditional_edges are StateGraph's write interface; they are not complicated in themselves. The complex part is the "magic" of inferring schemas and destinations from function signatures. Once you understand these three methods, you have essentially read all the mutable APIs of StateGraph. The next step is how compile translates these specs into PregelNode; for the runtime model see Pregel engine.

See official docs: LangGraph · README.