n4nAI

LangGraph nodes and edges explained with examples

LangGraph nodes and edges define agent control flow as a stateful graph. Learn how they work, why they matter, and see runnable code examples.

n4n Team4 min read959 words

Audio narration

Coming soon — every post will get a voice note here.

LangGraph nodes and edges are the primitive building blocks for constructing stateful, cyclic computation graphs in the LangGraph framework. A node is a callable that reads from and writes to a shared state object; an edge defines the transition rule between nodes, either unconditionally or via a routing function that selects the next node at runtime.

What LangGraph nodes and edges actually are

A LangGraph graph is a StateGraph instance parameterized by a state type. The state is a typed container—usually a Pydantic model or a TypedDict—that persists across node executions. Nodes are functions (or runnables) that accept the current state and return a partial state update. Edges connect nodes and determine control flow.

The framework ships two special sentinel nodes: START and END. Every graph must have at least one edge from START to a real node, and at least one path to END. Beyond that, you can wire arbitrary directed edges, including cycles.

from langgraph.graph import StateGraph, START, END
from typing import TypedDict

class State(TypedDict):
    messages: list

g = StateGraph(State)

That three-line snippet is the skeleton. Everything else is adding nodes and edges.

How execution works

The compiled graph is a state machine. When you call app.invoke(input), the runtime starts at START, follows edges, and stops at END. Each node execution is atomic from the reducer’s perspective—if a node raises, the state is unchanged unless you catch the error inside the node.

State and reducers

LangGraph applies node returns to the state using a reducer. For a plain field, the returned value replaces the field. For fields annotated with Annotated[list, add_messages], the reducer merges instead of overwriting. This matters when multiple nodes append to a message log.

from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]

If you omit the reducer, a node that returns {"messages": [msg]} wipes prior history. That bug surfaces as amnesia in your agent.

Node signatures

A node is any callable with signature (state) -> partial_state. It can be a plain function, a LangChain runnable, or a class with __call__. The graph invokes it with the current state and expects a dict (or None to make no change).

def router(state: State) -> dict:
    last = state["messages"][-1].content
    if "refund" in last.lower():
        return {"route": "billing"}
    return {"route": "general"}

Nodes never call other nodes directly. They return data; edges move control.

Edge types

Three edge primitives exist:

  1. Direct edge: g.add_edge("a", "b") always goes from a to b.
  2. Conditional edge: g.add_conditional_edges("a", routing_fn, {"b": "b", "c": "c"}) calls routing_fn(state) and uses its string output to pick the target.
  3. START/END: g.add_edge(START, "a") and g.add_edge("b", END).

Conditional edges are where LangGraph nodes and edges earn their keep. You encode branching logic as a pure function, not hidden inside a monolithic prompt.

Why explicit graphs matter for agents

Implicit agent loops—while True: action = llm(); tool(action)—are fine for a prototype. They break down when you need to audit a decision, inject a human approval step, or recover from a specific node failure.

LangGraph nodes and edges make the control flow a first-class artifact. You can visualize the graph, unit-test a node in isolation, and add a conditional edge to END that triggers on a guardrail check. The state object is the only coupling between nodes, which keeps them decoupled and testable.

Because each transition is an edge, tracing tools can show exactly which node produced a given state delta. In a flat loop, you get a stack trace; in a graph, you get a path. That difference compounds when the agent spans dozens of capabilities.

When a node calls an LLM, point it at an OpenAI-compatible gateway (for example, n4n.ai) to get automatic fallback across providers without rewriting node code. The node just sees a chat completion response.

Concrete example: a triage agent

We build a small graph that classifies inbound messages and routes to either a billing handler or a general handler, then exits.

State definition

from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    route: str

Nodes

from openai import OpenAI

# Use a gateway for resilient model access
client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="ENV_KEY")

def classify(state: AgentState) -> dict:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "system", "content": "Classify as billing or general."},
                  state["messages"][-1]]
    )
    label = resp.choices[0].message.content.strip().lower()
    return {"route": "billing" if "billing" in label else "general"}

def billing_node(state: AgentState) -> dict:
    return {"messages": [{"role": "assistant", "content": "Transferring to billing."}]}

def general_node(state: AgentState) -> dict:
    return {"messages": [{"role": "assistant", "content": "Handling generally."}]}

Edges

from langgraph.graph import StateGraph, START, END

g = StateGraph(AgentState)
g.add_node("classify", classify)
g.add_node("billing", billing_node)
g.add_node("general", general_node)

g.add_edge(START, "classify")
g.add_conditional_edges("classify", lambda s: s["route"],
                        {"billing": "billing", "general": "general"})
g.add_edge("billing", END)
g.add_edge("general", END)

app = g.compile()

Running it

result = app.invoke({"messages": [{"role": "user", "content": "I need a refund"}]})
print(result["route"])  # billing

This graph is cyclic-capable: if we wanted to loop back to classify after a handler, we would add g.add_edge("billing", "classify") and adjust terminal conditions. LangGraph nodes and edges permit that without restructuring the code.

Common misconceptions

Edges transport data between nodes

They do not. Edges only move control. Data flows through the shared state object. A node returns a dict; the reducer merges it into state; the next node reads from state. If you try to “pass arguments” via an edge, you are using the wrong abstraction.

Nodes must contain LLM calls

A node is just a function. It can call an API, query a database, sleep, or raise an exception. In many production graphs, the majority of nodes are deterministic glue: schema validation, retry counters, or format conversion.

Cycles are forbidden

LangGraph explicitly supports cycles. That is the “graph” part, not a DAG. The caveat: you must provide a termination condition (an edge to END or a conditional that eventually selects END), or the graph will loop forever at runtime.

START and END are optional sugar

They are required sentinels. START is where the runtime injects the initial state; END signals completion. You can name other nodes anything, but every path must originate at START and terminate at END (or the graph will raise at compile time if dangling).

Conditional edge routing functions must be pure

They should be side-effect free. Because LangGraph may re-execute nodes on rollback or in future distributed runners, a routing function that writes to a database will cause duplicate writes. Put side effects in nodes.

Nodes run in parallel by default

They do not. The default executor runs one node at a time per state. Parallelism requires explicit Send API usage or custom runners. Do not assume concurrent execution unless you configured it.

Debugging tips

When a graph misbehaves, print the state at each node boundary by wrapping nodes:

def traced(node_fn):
    def wrapper(state):
        out = node_fn(state)
        print(f"{node_fn.__name__} -> {out}")
        return out
    return wrapper

Compile with a checkpointer to replay steps. The explicit edge structure means you can also render the graph with app.get_graph().draw_mermaid() and confirm the topology matches your intent.

Where to go next

Treat LangGraph nodes and edges as the type system for agent control flow. Define a minimal state, keep nodes single-purpose, and push branching into conditional edges. That discipline keeps a 50-node agent maintainable long after the prototype prompt is forgotten.

Tagslanggraphai-agentsllm-basics

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langgraph for agent workflows posts →