Runtime & Context: Dependency Injection
Runtime[Context] is LangGraph's typed dependency injection system — giving every node and tool access to per-run configuration, cross-thread storage, execution metadata, and transient state through a single parameter. This article covers the full data placement decision (including UntrackedValue for non-serializable objects), the corrected subgraph propagation behavior, and the production failure modes that trip teams on first deployment.
Quick Reference
- →Runtime[T] properties: context, store, stream_writer, previous, execution_info, server_info — state is NOT on Runtime
- →State is a separate function parameter: def node(state: State, runtime: Runtime[Context])
- →ToolRuntime[T] adds: state, config, tools, tool_call_id — import from langgraph.prebuilt, NOT langchain.agents
- →context_schema defines immutable per-run data (user IDs, plan tier, feature flags) — validated at invocation
- →UntrackedValue marks state fields that must NOT be checkpointed — use for DB connections, caches, HTTP clients
- →Context auto-propagates to compiled subgraphs (LangGraph >= 0.6.0) — wrapper functions break propagation
- →runtime.execution_info gives you thread_id, run_id, attempt_number without parsing config
- →runtime.previous (functional API only) returns the previous return value for checkpoint recovery
Context vs State vs Store vs UntrackedValue
Before writing any Runtime code, answer one question: where does this data belong? Getting it wrong means either using mutable data as if it were immutable (context mistake), storing ephemeral data across threads (store mistake), accessing runtime properties that don't exist (state mistake), or putting non-serializable objects into checkpoints (UntrackedValue mistake).
state = changes during run · store = cross-thread persistence · context = fixed per-run · UntrackedValue = transient, not checkpointed
| Data example | Correct place | Why |
|---|---|---|
| user_id, org_id, plan tier | runtime.context | Set once at invocation, never changes during the run |
| Messages accumulated so far | state (function param) | Grows with each node step |
| Running token count | state (function param) | Updated by each node |
| User preferences learned over time | runtime.store | Needs to survive across separate runs/threads |
| API key reference (NOT the key itself) | runtime.context | Fixed per-run, but store secrets in env vars |
| DB connection pool | UntrackedValue (state field) | Must not be serialized; reconstructed each run |
| HTTP client session | UntrackedValue (state field) | Transient, not checkpoint-safe |
| Current tool call ID | ToolRuntime.tool_call_id | Injected automatically in tools |
A common mistake from reading old articles: Runtime does not have a .state property. State is the FIRST parameter of your node function — def node(state: MessagesState, runtime: Runtime[UserContext]). Runtime gives you context, store, stream_writer, previous, execution_info, and server_info. That's it.
UntrackedValue is a LangGraph state annotation that marks a field as transient — it exists during execution but is never written to a checkpoint. Use it for database connections, HTTP clients, or caches that can't be serialized. Define it as a state field (not on context) and reconstruct it at the start of each run.