Separating configuration from state keeps state clean and lets one compiled graph serve many users and settings.
Some information is needed by nodes but isn't part of the evolving state: the current user's ID, which model to use, a database connection, feature flags. LangGraph calls this runtime context.
1. Declare its shape, usually with a dataclass, and give it to the builder: StateGraph(State, context_schema=Context). 2. Pass a value per run: graph.invoke(inputs, context=Context(user_id="42")). 3. In a node, add a parameter runtime: Runtime[Context] and read runtime.context.user_id. LangGraph injects it automatically.
The Runtime object also gives nodes access to the store (long-term memory, later lesson) and the stream writer.
Why not put it in state? State is saved in every checkpoint and can be changed by nodes; context is read-only for the run and isn't saved. That makes it the right place for secrets-adjacent or per-request settings.
The older way to pass such values was config["configurable"]. That dict still carries LangGraph's own keys such as thread_id, but for your own settings context is now the recommended, type-checked route.
Other useful config keys: recursion_limit, max_concurrency (cap on parallel tasks), and tags/metadata that show up in traces.
Example
from dataclasses import dataclass
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.runtime import Runtime
@dataclass
class Context:
user_id: str
tone: str = "friendly"
def greet(state: MessagesState, runtime: Runtime[Context]) -> dict:
ctx = runtime.context
return {"messages": [{"role": "assistant", "content": f"Hi user {ctx.user_id} ({ctx.tone})"}]}
b = StateGraph(MessagesState, context_schema=Context)
b.add_node(greet)
b.add_edge(START, "greet")
b.add_edge("greet", END)
graph = b.compile()
out = graph.invoke({"messages": [{"role": "user", "content": "hello"}]},
context=Context(user_id="42", tone="formal"))
print(out["messages"][-1].content) # Hi user 42 (formal)Remember
- context_schema + invoke(..., context=...) passes run-wide settings
- Nodes read it via a runtime: Runtime[Context] parameter
- Context is read-only and not checkpointed, unlike state
- thread_id still goes in config['configurable']