Coming from LangGraph¶
nae is LangGraph underneath — AgenticGraph subclasses StateGraph and
compiles down to one. Nothing you know is wasted: nae replaces the imperative
add_node/add_edge/add_conditional_edges/Send calls with a declarative
> layer and adds a build-time validator on top. This page maps the LangGraph
API you already use to its nae equivalent.
Mapping table¶
| LangGraph | nae | Notes |
|---|---|---|
builder.add_node("a", fn) |
a = AgentNode(name="a", llm=llm, ...) |
The node is the object; you don't register it by name. |
builder.add_edge("a", "b") |
a > b |
> sets a.children = [b] and returns b, so a > b > c chains. No edge exists until AgenticGraph(...) walks the chain. |
builder.add_edge(START, "a") |
AgenticGraph(start_node=a, ...) |
The start is a constructor arg, not an edge. |
builder.add_edge("z", END) |
AgenticGraph(..., end_nodes={z}) |
Termination is controlled by end_nodes, not by .child == None. |
fan-out: two add_edge("a", ...) calls |
a > fanout(b, c) > d (or a > [b, c] > d) |
Branches run concurrently; the join d is deferred until all finish (fires once, not once per branch). |
add_conditional_edges("a", router_fn, [...]) |
DecisionNode (LLM) or ConditionNode (predicate) |
Index the choices to wire them: decide["yes"] > handler. |
Send("worker", payload) + a conditional edge |
dispatch > worker.map("field") > reduce |
.map("field") fans worker out over state["field"], one Send per item, in parallel, then joins once at reduce. |
ToolNode([tools]) + tools_condition + loop-back edge |
AgentNode(tools=[...]) |
The tool-call loop (execute → append ToolMessage → re-invoke) is built into the node, capped by max_tool_iterations. |
with_structured_output(Literal[...]) + a router fn |
DecisionNode(choices=[...]) |
nae builds the Literal structured output and the conditional edges; the model can only return a valid branch. |
builder.compile() |
happens inside AgenticGraph(...) |
Construction walks the wiring, runs the validator, and compiles. |
a subgraph added via add_node(compiled_subgraph) |
graph_0 > graph_1 |
An AgenticGraph composes as a node inside a larger AgenticGraph. |
MessagesState |
AgenticState (the default) |
messages + reduced log / token channels; routers auto-register a per-node decision_{name} key. Schema-free by default; nodes auto-register keys. |
— (runtime KeyError when you get it wrong) |
compile-time GraphValidationError |
The dataflow validator has no LangGraph equivalent. See the validator. |
When to use which¶
>(sequential edge) — one step then the next. The 90% case.fanout(a, b, c)/[a, b, c]— a fixed, known set of branches that should run in parallel and rejoin (e.g. three critics, then a verdict). Both forms wire identically;fanout(...)reads better inline.worker.map("field")— a dynamic number of parallel tasks: one worker applied to every item of a state list (map-reduce / orchestrator-workers). Use this instead of hand-rollingSend.DecisionNode— the branch should be chosen by an LLM (classify, triage, route). Constrained to valid choices via structured output. Can taketools=to gather context before routing.ConditionNode— the branch is decided by plain Python (a counter, a flag, a length check). Free, deterministic, no model call — ideal for capping loops.AgentNode(tools=[...])— any tool-using / ReAct agent. Replaces theToolNode+tools_condition+ loop-back triad with one node.- Drop back to raw LangGraph anytime — the compiled graph is a real
StateGraph, so streaming, async,durability, checkpointers, and LangSmith all work unchanged. No lock-in.
Under the hood¶
a > b doesn't build an edge — it sets a.children = [b] and returns b, so
a > b > c builds a linked structure in memory. AgenticGraph(...) walks that
structure once at construction, emits the real LangGraph
add_node/add_edge/Send calls, runs the dataflow validator, and
compile()s to a native StateGraph — which AgenticGraph subclasses.
So nae is a build-time convenience that disappears at runtime: the execution underneath is plain LangGraph, and you can drop down to it anytime.
vs. LangChain's Functional API¶
The closest comparison isn't raw graphs — it's LangChain's own Functional API
(@entrypoint / @task), which also lets you skip explicit graph wiring. The
difference is visibility of control flow:
Functional API (@entrypoint/@task) |
nae | |
|---|---|---|
| Control flow | hidden in plain Python (loops, if, await) |
an explicit, declarative DAG |
| Fan-out / join | you orchestrate futures by hand | fanout(...) / .map("field"), one line |
| Inspect the topology | run it and trace | summary() prints the static DAG |
| Catch dataflow bugs | at runtime | at compile time, before any invoke |
Both are concise. The trade nae makes is keeping the topology inspectable and validatable: you can see the fan-out/join/map-reduce structure, render it, and have the build reject read-before-written bugs — where the Functional API hides the graph inside ordinary Python.
Why not raw LangGraph?¶
You keep the entire LangGraph runtime either way — nae disappears at compile
time. What it buys you: (1) less wiring boilerplate, (2) an inspectable
topology (summary() prints the DAG, and the graph renders itself in a
notebook), and (3) a dataflow validator that rejects
read-before-write and unwired-branch bugs before you spend a token — which has
no equivalent in raw LangGraph.
A worked comparison — the same tool-using agent in 3 lines vs. 10 — is on the
home page, and every file in the
Examples galleries pairs the nae version with a
# --- equivalent in raw LangGraph --- block.