Adding custom metadata to runs
Why this matters
In production, you need to correlate graph runs with user IDs, request tracing, A/B test variants, or cost tracking. Metadata lets you do this without mixing business context into your core state machine, keeping your graph logic clean and reusable.
Explanation
Metadata is a dictionary you pass to graph.invoke() via the config parameter that travels with your run without being part of the state. It's accessible in node functions via run_manager.config['metadata'] or through the LangChain callback system. Mechanically, when you call invoke(input, config={'metadata': {...}}), langgraph attaches this to the invocation context. Any node that accesses the run manager can read it, and if you're using a checkpointer (like MemorySaver), the metadata is logged alongside the state snapshots, making it queryable for debugging or analytics. When to use it: session IDs, user IDs, experiment tags, request trace IDs, cost centers, or any observability context that applies to the entire run but doesn't belong in your message or state schema.
Analogy
Metadata is like writing your name and project code on the envelope of a letter, but keeping the letter itself blank. The envelope travels with your letter, gets logged at each post office (node), and you can look it up later: but the content of the letter (your state) stays independent and focused on its job.
Code
import json
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import BaseMessage, HumanMessage
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
from langgraph.checkpoint.memory import MemorySaver
class State(TypedDict):
messages: list[BaseMessage]
def log_metadata_node(state: State, config) -> State:
metadata = config.get('metadata', {})
user_id = metadata.get('user_id')
session_id = metadata.get('session_id')
experiment = metadata.get('experiment_tag')
print(f"[LOG] user_id={user_id}, session_id={session_id}, experiment={experiment}")
print(f"[LOG] Processing {len(state['messages'])} messages")
return state
def process_node(state: State, config) -> State:
metadata = config.get('metadata', {})
request_id = metadata.get('request_id')
print(f"[PROCESS] Request {request_id}: {state['messages'][-1].content if state['messages'] else 'empty'}")
return {"messages": state["messages"] + [HumanMessage(content="[processed]")]}
graph = StateGraph(State)
graph.add_node("log", log_metadata_node)
graph.add_node("process", process_node)
graph.add_edge(START, "log")
graph.add_edge("log", "process")
graph.add_edge("process", END)
compiled = graph.compile(checkpointer=MemorySaver())
initial_state = {"messages": [HumanMessage(content="Hello")]}
metadata_config = {
"metadata": {
"user_id": "user_42",
"session_id": "sess_abc123",
"experiment_tag": "variant_b",
"request_id": "req_xyz789"
}
}
result = compiled.invoke(initial_state, config=metadata_config)
print(f"\n[RESULT] Final message count: {len(result['messages'])}")
print(f"[RESULT] Messages: {[m.content for m in result['messages']]}") [LOG] user_id=user_42, session_id=sess_abc123, experiment=variant_b [LOG] Processing 1 messages [PROCESS] Request req_xyz789: Hello [RESULT] Final message count: 2 [RESULT] Messages: ['Hello', '[processed]']
What just happened?
We created a graph with two nodes. The first node (log) extracted metadata from config and printed user/session/experiment info. The second node (process) extracted the request_id from metadata and printed it alongside the current message. When we invoked the graph, we passed metadata via config={'metadata': {...}}. Each node received that metadata intact via config.get('metadata'), printed it, and the graph executed without metadata interfering with the state machine. The state remained clean: only messages flowed through it.
Common gotcha
Developers often try to modify metadata inside a node and expect it to persist for downstream nodes. Metadata is read-only during execution: you cannot update it mid-run. If you need mutable context that flows through the graph, it must go in your State, not metadata. Also, if you don't pass config explicitly, config.get('metadata', {}) returns an empty dict silently: it won't error, but you won't have your metadata either.
Error recovery
KeyError when accessing metadataMetadata appears empty or NoneMetadata changes in one node don't affect the nextExperienced dev note
In production langgraph systems, metadata + checkpointing is your secret weapon for observability. When you use MemorySaver or a persistent checkpointer, the metadata gets stored with each state snapshot. This means you can later query 'show me all runs where experiment_tag=variant_a and user_id=user_42' by reading checkpoint metadata: without modifying your graph code. This decouples analytics from logic. Also, if you're using a LangSmith integration, metadata automatically flows into traces, giving you free filtering and grouping in the dashboard.
Check your understanding
If a user accidentally modifies state['messages'] inside a node and returns a new state without the metadata, will downstream nodes still have access to the original metadata passed at invoke() time?
Show answer hint
Yes. Metadata is attached to the invocation context, not the state dict. State is what flows through the graph; metadata travels independently via config. Modifying state has no effect on metadata availability. This is the core design: metadata and state are separate.