Clarification needed: Assistant config vs context and graph initialization

Hi everyone,

I’m a bit confused about the respective roles of config and context when working with assistants in LangGraph Platform.

When creating an assistant through the API, I see both fields:

{
  "graph_id": "my_graph",
  "config": {},
  "context": {}
}

From the documentation and examples, it seems that assistant-specific values are typically accessed through runtime.context, using a context_schema.

However, this leaves me with a few questions:

  1. What is the intended purpose of the assistant-level config field?
  2. How does it differ from context in practice?
  3. Can either config or context be accessed during graph initialization (inside the graph factory / make_graph() function), or are they only available at runtime?
  4. If I want to customize the graph structure based on assistant settings, what is the recommended approach?

My current understanding is:

  • Neither context nor assistant configuration are available when the graph is being constructed.
  • Graph customization should therefore happen at runtime rather than during graph compilation.

Is that correct, or am I missing something?

Thanks!

Hi @gdrouet

great question. It’s been confusing to me for a long time though, I think I can explain it this way:

short: config is the legacy/runtime-plumbing channel (RunnableConfig: configurable, tags, recursion_limit), context is its typed v0.6+ successor for user-defined values, and - contrary to your current understanding - both can be available during graph construction if you register a graph factory instead of a pre-compiled graph.

  1. What is the assistant-level config field for?

It is a persisted RunnableConfig fragment. Per the Create Assistant API reference, assistant config has exactly three keys:

  • configurable: dict - arbitrary key/value configuration (the pre-v0.6 way to parameterize a graph)
  • tags: string[] - tracing tags applied to every run of this assistant
  • recursion_limit: int - execution knob for the Pregel loop

When a run is created, the server merges the assistant’s config into the run’s RunnableConfig. In the dev server (langgraph_runtime_inmem/ops.py, Runs.put) the run config is built as:

"config": Runs._merge_jsonb(
    assistant["config"],          # assistant-level config
    config,                       # run-level config (wins on conflicts)
    {"configurable": configurable},   # assistant -> thread -> run configurable
    {"metadata": merged_metadata},
),

and the configurable dict itself is merged as assistant.configurable -> thread.configurable -> run.configurable -> {run_id, thread_id, graph_id, assistant_id, user_id} - later sources win, so run-level values override assistant-level values.

So the intended purposes of assistant config today are:

  • carrying values for graphs that still read config["configurable"] (the pre-0.6 pattern - still fully supported for backward compatibility)
  • pinning execution knobs that context cannot express: tags and recursion_limit
  • feeding the graph-factory path (see Q3)

2. How does it differ from context in practice?

context is the typed replacement for user-defined config["configurable"] values, introduced in LangGraph v0.6:

  • You declare a context_schema on the graph (StateGraph(State, context_schema=Context) / create_agent(..., context_schema=Context)). In the langgraph source: libs/langgraph/langgraph/graph/state.py - context_schema parameter; the old config_schema parameter is deprecated since v0.6.0 with removal planned in v2.0.0
  • It travels as a separate channel, not inside config: graph.invoke(input, context=...) (libs/langgraph/langgraph/pregel/main.py, invoke(..., context: ContextT | None = None)), is coerced to your dataclass/Pydantic/TypedDict schema (_coerce_context), and is exposed to nodes/tools/middleware as runtime.context on the frozen Runtime dataclass (libs/langgraph/langgraph/runtime.py). Internally the Runtime rides along in config["configurable"]["__pregel_runtime"], which is why the two systems interoperate
  • On the assistant object, config and context are stored as two separate fields (AssistantBase in libs/sdk-py/langgraph_sdk/schema.py: config: Config, context: Context; the latter was added in SDK v0.6.0 - “Static context to add to the assistant”)
  • At run creation the server merges them independently: "context": Runs._merge_jsonb(assistant.get("context", {}), kwargs.get("context", {})) - run-level context wins. The merged context is then passed to the graph’s astream/ainvoke as the context argument (the server even filters it against your graph’s context JSON schema first - _filter_context_by_schema in langgraph_api/stream.py)

Practical rule of thumb:

config context
Typing untyped configurable dict validated against context_schema
Access in graph code config["configurable"]["x"] (legacy) runtime.context.x (current)
Extra knobs tags, recursion_limit -
Status supported, legacy for user values recommended since v0.6
Merge at run time assistant → thread → run (run wins) assistant → run (run wins)

The docs (Assistants, configuration how-to) now describe assistants almost entirely in terms of context - that is the recommended channel for “assistant settings” your nodes read. The v1 migration guide explicitly notes the old config["configurable"] pattern still works but context is recommended.

3. Can config/context be accessed during graph initialization?

Yes - if you register a graph factory. This is the part your current understanding misses.

If your langgraph.json points at a callable instead of a compiled graph ("my_graph": "./graph.py:make_graph"), the server rebuilds the graph via that factory every time it needs it. From langgraph_api/graph.py (installed server, v0.4.20):

FACTORY_ACCEPTS_CONFIG[graph_id] = len(inspect.signature(graph).parameters) > 0
...
value = value(config) if factory_accepts_config(value, graph_id) else value()

and from langgraph_api/stream.py - when a run executes, the factory receives the merged run config (assistant config → run config, including the merged configurable):

context = kwargs.pop("context", None)
config = cast(RunnableConfig, kwargs.pop("config"))   # already merged with assistant config
graph = await stack.enter_async_context(
    get_graph(configurable["graph_id"], config, ...)
)

So inside make_graph(config: RunnableConfig) you can read config["configurable"]["my_setting"] and it will reflect the assistant’s configurable (overridden by run-level values if provided). Note the factory is also invoked for non-run endpoints (assistant schemas, subgraphs, state reads) - e.g. langgraph_api/api/assistants.py loads assistant["config"] and calls get_graph(graph_id, config) for the schema endpoints - so assistant config is available there too.

context in the factory: on recent agent-server versions the factory can declare a second parameter, runtime: ServerRuntime (Rebuild graph at runtime; langgraph_sdk/runtime.py in the langgraph repo). The server inspects type hints and injects it. ServerRuntime exposes:

  • access_context - why the factory is being called ("threads.create_run", "assistants.read", "threads.read", …),
  • execution_runtime - non-None only when actually executing a run, and it carries context (the merged assistant → run context), the authenticated user, and the store.
async def make_graph(config: RunnableConfig, runtime: ServerRuntime):
    if ert := runtime.execution_runtime:
        ctx = ert.context          # merged assistant/run context - execution paths only
    ...

(The 2-arg factory shape is exercised end-to-end in the langgraph repo’s integration fixture libs/sdk-py/integration/graph/factory_graph.py.) On older servers (1-arg factory), context is not passed to the factory at all - it is only handed to the graph at invoke time - so on those versions only config["configurable"] is usable for construction-time decisions.

If you export a pre-compiled graph (no factory), then your statement is correct: the module is imported once at server startup and nothing assistant-specific exists at compile time.

Customizing the graph per assistant

In order of preference:

  1. keep one topology, vary behavior at runtime. Read runtime.context in nodes/conditional edges to pick models, prompts, tools, and routing. This is the pattern the docs push for assistants (configuration how-to) and it works with checkpointing, schema endpoints, and Studio with zero surprises. Conditional edges mean “different structure per run” rarely requires actually different graphs
  2. if construction itself must differ (e.g. tools discovered from an MCP server, per-tenant node sets), use a graph factory keyed off config["configurable"] - available on both execution and read paths - and/or runtime.execution_runtime for execution-only resources. Two caveats from the graph-rebuild docs:
    • the factory runs on every request that needs the graph (runs, state reads, schema fetches), so keep it fast
    • the returned graph should keep the same topology (nodes, edges, state schema) across access contexts - use execution_runtime to conditionally wire expensive resources, not to reshape the graph, otherwise schema/state endpoints can disagree with what actually ran
  3. if you truly need different topologies, register them as separate graphs in langgraph.json and create assistants against different graph_ids

So

  • “neither context nor assistant configuration are available when the graph is being constructed” - incorrect for factories: assistant config.configurable is passed to make_graph(config) (merged with run config on execution paths), and merged context is available via ServerRuntime.execution_runtime.context on recent server versions (execution paths only). Correct only for pre-compiled graph exports
  • “graph customization should therefore happen at runtime rather than during graph compilation” - right default, but as a recommendation, not a hard constraint: runtime branching via runtime.context first, graph factory when construction genuinely depends on assistant settings