LangGraph Node Anatomy — What a Single Node Can Contain
Status: ACTIVE (conversation observation) Agent: opencode/ext-agent (sandshrew) Timestamp UTC: 2026-05-11T18:45:00Z Claim: synthesis | 2026-05-11T18:40:00Z Session: MjF conceptual exploration — cataloging everything a LangGraph node can hold, reference, emit, and obey
Prior Context
- [[langgraph-core-mechanisms]] (Pearl Brain: research-sandshrew-20260511-180837)
- [[langgraph-game-surface]] — game backend architecture
- This page: raw node anatomy. Not mapped to game concepts yet — just what exists.
1. INPUT: What Arrives at the Node
State (current graph snapshot)
The full state dictionary. All keys across all schemas (input, output, internal, private) are accessible. A node can READ any channel even if its declared input schema doesn't include it. It can only WRITE channels it references in its return — but it can write to channels outside its input schema (the graph state is the union of all schemas at init).
Config (execution metadata)
| Field | What It Is | Example |
|---|---|---|
thread_id |
Session/run identifier | "game-session-42" |
configurable |
User-provided arbitrary dict | {"model": "claude", "difficulty": "hard"} |
metadata.langgraph_step |
Current super-step counter | 17 |
metadata.langgraph_node |
Name of the currently executing node | "hex_17" |
metadata.langgraph_triggers |
What triggered this node | ["hex_16"] (which node sent the message) |
metadata.langgraph_path |
Path through graph so far | ["START", "planning", "hex_16", "hex_17"] |
metadata.langgraph_checkpoint_ns |
Current checkpoint namespace | "" or subgraph NS |
recursion_limit |
Max super-steps before error | 1000 (default) |
tags |
Tracing tags for observability | ["production", "combat"] |
Runtime (operational services)
| Field | What It Provides | Tangible Use |
|---|---|---|
context |
Per-invocation typed runtime context | DB pool, model client, MCP endpoint handle — injected at .invoke(context=...) |
store |
LangGraph Store: cross-thread key-value persistence | Save state visible to OTHER graphs/threads |
stream_writer |
Emit streaming chunks mid-execution | "Unit is thinking..." progress to UI |
execution_info |
Thread ID, graph info | Logging, correlation |
heartbeat |
Idle timeout refresh | Keep-alive during long LLM calls |
control |
Graceful shutdown (RunControl.request_drain) |
Stop cleanly at next super-step |
server_info |
Deployment metadata | Version, environment |
2. PROCESS: What the Node Can Do
Deterministic Logic
Computations, data transformations, validations — anything Python. This is where game rules live: damage calculation, movement validation, fog-of-war updates, inventory checks.
Side Effects (External Calls)
- LLM invocation (via LangChain ChatModel, or raw HTTP, or any harness)
- Tool calls / MCP calls (direct HTTP, or wrapped)
- File I/O (reading maps, writing logs)
- Database reads/writes (outside of LangGraph state)
- External service calls
- Store writes (cross-thread persistence)
None of these require LangChain. The node is a plain Python function.
Control Flow (Graph Manipulation)
| Mechanism | What It Does | When Used |
|---|---|---|
return dict |
State update, follow static edges | Standard path |
return Command(update=..., goto=...) |
State update + dynamic routing override | "Move to hex 17 AND check for ambush" |
return Command(goto=..., graph=Command.PARENT) |
Navigate to parent graph node | Subgraph → parent handoff |
interrupt(value) |
Pause graph, return value to caller, await resume | Human decision point |
return None |
No state change, follow static edges | Pass-through / no-op |
return [Send("target_node", state_slice)] |
Fan-out: spawn parallel node executions | "All units in this hex attack" |
What It Cannot Do
- Create new nodes at runtime (topology is compile-time)
- Modify the graph structure during execution
- Access state from other threads without the Store
- Resume itself after interrupt — that requires external
Command(resume=...)
3. OUTPUT: What the Node Emits
State Update (return dict)
A partial update. Only keys that changed. Merged via the key's reducer:
- Default reducer → overwrite
- Annotated[list, operator.add] → append
- Annotated[list, add_messages] → append + deduplicate by message ID
- Custom reducer → user-defined merge logic
Node can write to ANY state channel regardless of declared input schema — the graph state is the union of all schemas.
Return Type Variants
| Return | Effect |
|---|---|
{"key": value} |
State update, static edges execute |
Command(update={"key": value}, goto="node_x") |
State update + explicit next node |
Command(goto="node_x", graph=Command.PARENT) |
Jump to parent graph |
None or no return |
No state change, static edges execute |
Side-Stream Output
stream_writer("chunk")— real-time streaming to caller- Store writes — cross-thread persistence
- Logs, traces
4. REFERENCES: What the Node Can Access
Internal (Graph-Aware)
| Reference Type | Access Pattern | Example |
|---|---|---|
| Other node names | String literals in Command/Send | goto="hex_17" |
| State channels | dict key access on state | state["units"], state["fog_map"] |
| Edge topology | Implicit via return routing | Conditional edge function decides next |
| Subgraphs | Command.PARENT, StateGraph nesting |
Battle subgraph, dialogue subgraph |
| Checkpointer | State persistence, time travel | graph.get_state(config, checkpoint_id=n) |
| Recursion counter | config["metadata"]["langgraph_step"] |
Proactive limit handling |
RemainingSteps |
Managed value in state | "2 steps left, wrapping up" |
External (Runtime-Injected)
| Reference Type | Access Pattern | Example |
|---|---|---|
| Context object | runtime.context |
Model client, DB pool, MCP server handle |
| Config dict | config["configurable"] |
User preferences, feature flags |
| Store | store.get/put/search |
Cross-thread key-value data |
| Any Python import | Standard imports | import requests, import pygame |
Node-Local (Fragile)
A node can reference its own function body, closure variables, or module-level globals. This works but is fragile — state persists only in LangGraph's State, not in Python memory across invocations. Closure variables may not survive graph serialization or thread migration.
5. CONSTRAINTS: What the Node Must Obey
| Constraint | Detail |
|---|---|
| Function signature | (state: State, config: RunnableConfig, runtime: Runtime[Context]) — state required, others optional |
| Return type | Must be dict, Command, or None — must not break state schema |
| Write scope | Can write to any channel regardless of declared input schema |
| Type annotations | Required for Command return types (Command[Literal["node_a", "node_b"]]) — used for graph rendering |
| interrupt() dependency | Graph must be compiled with a checkpointer |
| Static + dynamic edges | If a node returns Command(goto=...) AND has static edges via add_edge, BOTH execute. Don't mix. |
| Max step guard | recursion_limit caps total super-steps. Node can monitor via config["metadata"]["langgraph_step"] |
| Serialization | State must be serializable if using checkpointer. Not all Python objects survive serialization. |
| Caching | Can be cached per-input via CachePolicy(ttl=...) in add_node |
Summary: Node == Function + Access + Side Effects + Routing
A LangGraph node is a Python function that: - Receives the full graph state (read access to everything) - Receives execution context (thread ID, step counter, user config, runtime dependencies) - Runs arbitrary Python logic (LLM calls, rules, HTTP, DB, I/O) - Emits a partial state update (merged via per-key reducers) - Optionally overrides routing (Command.goto) or pauses execution (interrupt) - Can fan out parallel work (Send) - Can reference other nodes by name, state channels by key, external dependencies via context
It is not a "prompt template" or a "tool binding" or a "task definition" — it is a general-purpose function with graph-aware superpowers. Everything else (prompts, tools, configs, task logic) lives inside the function body, organized however the developer wants.