Runtime Context: Injecting User-Specific Data
Right now, Aria behaves exactly the same no matter who's talking to her. That's fine while she's only helping Julie — but what if Aria eventually helped Julie's whole team? Tom shouldn't get replies signed "Best, Julie." Each person needs Aria to know a few fixed facts about them, specifically — without that information needing to be typed out in every single message.
That's what runtime context is for. In this article, you'll learn how to hand an agent fixed, user-specific information at the moment you call it — and just as importantly, how this is genuinely different from the memory you built in Article 4.
🟡 Skill level: Intermediate.
Quick Reference
When to use this: Whenever an agent needs to know fixed facts about who is currently using it — preferences, identity, configuration — without that information coming from the conversation itself.
Basic syntax:
from dataclasses import dataclass
from langchain.agents import create_agent
@dataclass
class UserContext:
user_name: str = "Julie"
email_signature: str = "Best, Julie"
agent = create_agent(model="gpt-5-nano", context_schema=UserContext)
response = agent.invoke(
{"messages": [...]},
context=UserContext(user_name="Tom", email_signature="Cheers, Tom"),
)
Common patterns:
- Context is defined as a Python
dataclass, then passed in with eachinvoke()call - Tools access context through
runtime.context, usingToolRuntime - Context is fixed for the duration of one call — unlike memory, it doesn't accumulate or change as the conversation goes on
Gotchas:
- ⚠️ Context is not memory — it doesn't persist between calls or change during a conversation. It's set once, when you invoke, by your own code.
- ⚠️ If you don't pass a
context=...value, the dataclass's default values are used instead — make sure your defaults make sense, or you may not notice when context wasn't actually customized.
See also: Memory and Threads: Agents That Remember
What You Need to Know First
- Everything from Articles 1–6, especially tool calling from Article 2 and memory from Article 4
- Basic familiarity with Python's
dataclassis helpful — we'll explain what's needed as we go
What We'll Cover in This Article
- What runtime context is, and exactly how it differs from memory
- How to define a context schema using a
dataclass - How to access context from inside a tool using
ToolRuntime - How the same agent can behave differently for different people, using only context
What We'll Explain Along the Way
- What a Python
dataclassis, for readers who haven't used one before - The conceptual difference between context, state, and memory (this gets a dedicated section, since it's the easiest thing in this article to confuse)
What Is Runtime Context, and How Is It Different From Memory?
This is worth being precise about, because the two concepts are easy to mix up.
Memory (from Article 4) is information that accumulates as a conversation happens — what was said, turn by turn — and gets saved and reloaded by a checkpointer across separate invoke() calls.
Context is different: it's fixed information you decide and hand to the agent at the moment you call it, and it stays exactly the same for that entire call. It doesn't come from the conversation, and it doesn't change as the conversation unfolds.
Think of it like walking into a building wearing a name badge. The badge doesn't change while you're inside, no matter what you say or do in there — it was fixed before you walked in, by whoever issued it. Memory is more like the building's logbook of what happened during your visit; context is the badge you were already wearing when you arrived.
Diagram: Context is set once by your code at the start of a call and stays fixed throughout. The conversation itself accumulates and can be saved as memory across calls — a separate concern entirely.
Defining a Context Schema
Let's define what fixed, per-user information Aria needs. We'll use a Python dataclass — a quick way to define a small class whose entire job is holding a few named values, without writing boilerplate code by hand.
# Purpose: Define the shape of fixed, user-specific data Aria needs
# Context: Each field here will be set once per invoke() call, not per message
# Input: N/A — this defines a shape, not a specific value yet
# Output: A reusable UserContext class with sensible defaults
from dataclasses import dataclass
@dataclass
class UserContext:
user_name: str = "Julie"
email_signature: str = "Best, Julie"
Each field has a default value, which means UserContext() with no arguments is valid and gives you Julie's settings — useful while Aria is only serving one person, and a sensible fallback even once she serves more.
Now let's tell create_agent about this shape, using context_schema:
# Purpose: Configure an agent to expect UserContext at invocation time
# Context: This doesn't set any actual values yet — just declares the shape
# Input: N/A
# Output: An agent instance aware of the UserContext schema
from langchain.agents import create_agent
agent = create_agent(
model="gpt-5-nano",
context_schema=UserContext,
)
And finally, pass an actual UserContext value when invoking:
# Purpose: Invoke the agent with a specific context value
# Context: This call is specifically for Julie, using her default settings
# Input: A question, plus a UserContext instance
# Output: A response — context isn't used by anything yet, so behavior is unchanged
from langchain.messages import HumanMessage
response = agent.invoke(
{"messages": [HumanMessage(content="Draft a quick reply to Jane.")]},
context=UserContext(),
)
At this point, the context is being passed in, but nothing is actually using it yet — the agent itself doesn't automatically read context into its responses. For that, we need a tool that explicitly looks at it.
Accessing Context Inside a Tool
Tools can read the current context through ToolRuntime, the same object you'll recognize conceptually from inspecting tool_calls in Article 2 — except now it's giving the tool information, not just metadata about the call.
# Purpose: Define a tool that reads the current user's signature from context
# Context: Lets Aria sign off correctly, based on who she's currently helping
# Input: None directly — pulls from runtime.context instead
# Output: The signature string for whoever the current context represents
from langchain.tools import tool, ToolRuntime
@tool
def get_user_signature(runtime: ToolRuntime) -> str:
"""Get the email signature to use for the current user."""
return runtime.context.email_signature
Notice this tool takes a runtime: ToolRuntime parameter instead of (or alongside) the kinds of parameters we used in Article 2. LangChain recognizes this special parameter and automatically provides the current context through runtime.context — you don't pass it manually.
Let's wire this into Aria properly:
# Purpose: Give Aria the signature tool, and watch her use the right context
# Context: Aria now signs replies correctly based on who's using her
# Input: A request to draft a reply
# Output: A reply signed with Julie's specific signature
from langchain.agents import create_agent
from langchain.messages import HumanMessage
aria_system_prompt = """
You are Aria, a personal email assistant for Julie.
You are warm, concise, and a little formal.
Always sign off replies using the get_user_signature tool — never guess
or make up a signature.
"""
agent = create_agent(
model="gpt-5-nano",
system_prompt=aria_system_prompt,
tools=[get_user_signature],
context_schema=UserContext,
)
response = agent.invoke(
{"messages": [HumanMessage(content="Draft a quick reply to Jane confirming coffee.")]},
context=UserContext(),
)
print(response["messages"][-1].content)
The reply should now end with "Best, Julie" — not because the model guessed it, but because the tool looked it up from the context you explicitly provided.
Different Context, Different Behavior
Here's the actual payoff: the exact same agent object can now serve a completely different person, just by passing a different context.
# Purpose: Demonstrate the same agent serving a different person via context
# Context: No new agent was created — only the context value changed
# Input: The same kind of request, for a different user
# Output: A reply signed correctly for Tom instead of Julie
response = agent.invoke(
{"messages": [HumanMessage(content="Draft a quick reply to Jane confirming coffee.")]},
context=UserContext(user_name="Tom", email_signature="Cheers, Tom"),
)
print(response["messages"][-1].content)
Same agent, same code, same tool — but the signature comes back as "Cheers, Tom" this time. Nothing about the agent itself changed; only the context passed into this specific call did.
Common Misconceptions
❌ Misconception: Context is just another word for memory
Reality: Memory (Article 4) accumulates automatically from the conversation and persists across separate invoke() calls via a checkpointer. Context is fixed information your own code decides and passes in explicitly, every single call — it never accumulates, and it doesn't persist on its own.
Why this matters: If you're expecting context to "remember" something from an earlier call without you explicitly passing it again, it won't — you have to supply it every time, by design.
Example:
# ❌ Wrong assumption: "context will remember what I passed last time"
agent.invoke({"messages": [...]}) # no context passed — uses defaults, not "what was set before"
# ✅ Correct: context must be explicitly passed every single call
agent.invoke({"messages": [...]}, context=UserContext(user_name="Tom", ...))
❌ Misconception: Context can change mid-conversation
Reality: Context is fixed for the entire duration of one invoke() call — it's set once, at the start, by your code. It cannot be changed by the agent or a tool partway through processing a single call.
Why this matters: If you need information that can change during a conversation — based on what a tool discovers, for instance — that's a different concept entirely (custom agent state), which is exactly what the next article covers.
Troubleshooting Common Issues
Problem: A tool using runtime.context raises an AttributeError
Symptoms: An error like "UserContext has no attribute..." or similar, when a tool tries to read runtime.context.
Common Causes:
- The field name in the tool doesn't match the dataclass field name exactly (most common — e.g.,
runtime.context.signaturevs. the actual fieldemail_signature) - No
context_schemawas set oncreate_agent, soruntime.contextdoesn't have the shape you expect
Diagnostic Steps:
# Step 1: Confirm the dataclass field names exactly
@dataclass
class UserContext:
user_name: str = "Julie"
email_signature: str = "Best, Julie" # exact field name to use
# Step 2: Confirm context_schema is set on create_agent
agent = create_agent(model="gpt-5-nano", context_schema=UserContext) # ✅
Solution: Match field names exactly between your dataclass and wherever you access runtime.context.<field>, and confirm context_schema is actually set.
Prevention: Keep your context dataclass and the tools that read from it close together in your code, so a field rename is easy to catch everywhere it's used.
Problem: Context doesn't seem to change behavior at all
Symptoms: Passing a different context=... value doesn't seem to affect the response.
Common Causes:
- No tool actually reads
runtime.context— context alone doesn't automatically influence the model's behavior unless something explicitly uses it (most common) - The system prompt doesn't direct the agent to use the relevant tool
Solution: Remember that context is just data — something has to actually read it (a tool, as shown here) for it to have any effect on the response.
Prevention: After defining a new context field, immediately write a small tool that reads it and test that specific tool in isolation before assuming the agent will use it correctly end-to-end.
Check Your Understanding
Quick Quiz
-
What's the core difference between context and memory?
Show Answer
Memory accumulates automatically from the conversation and persists across separate calls via a checkpointer. Context is fixed information your code explicitly passes in at the start of each call — it doesn't accumulate and doesn't persist unless you pass it again yourself.
-
If you call
agent.invoke({"messages": [...]})without passingcontext=..., what happens?Show Answer
The dataclass's default field values are used instead — in our example, that means
UserContext()'s defaults (Julie's name and signature) apply automatically. -
Can a tool change the context value partway through a conversation?
Show Answer
No — context is fixed for the entire duration of one
invoke()call. Information that needs to change during a conversation is a different concept (custom agent state), covered in the next article.
Hands-On Exercise
Challenge: Add a timezone: str field to UserContext (default "America/New_York"), and write a tool get_user_timezone that returns it, so Aria could eventually use it for scheduling-related replies.
Show Solution
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
@dataclass
class UserContext:
user_name: str = "Julie"
email_signature: str = "Best, Julie"
timezone: str = "America/New_York"
@tool
def get_user_timezone(runtime: ToolRuntime) -> str:
"""Get the current user's timezone."""
return runtime.context.timezone
Explanation: Adding a new field to the dataclass and a matching tool follows exactly the same pattern as email_signature — context schemas are meant to grow this way as your agent needs more fixed, per-user facts.
Summary: Key Takeaways
- Runtime context is fixed, user-specific information your code passes in at the moment you call
invoke() - Context is defined with a
dataclassand declared oncreate_agentviacontext_schema - Tools read context through
runtime.context, using aToolRuntimeparameter - Context is fixed for the whole call — unlike memory, it doesn't accumulate or persist on its own
- The same agent can serve different users differently, just by passing different context values
- Aria can now adapt her behavior (like signing off correctly) based on who she's currently helping
Version Information
Tested with:
- Python:
>=3.10, <4.0 langchain:>=1.1.3(latest stable as of writing:1.3.4)dataclasses— part of the Python standard library, no installation needed
Known issues:
- None specific to this article's functionality at the time of writing.
What's Next?
You now understand how to give an agent fixed, per-user context — and importantly, how that's different from memory.
The natural next step is Custom Agent State: Reading and Writing Beyond Messages — context is fixed for a whole call, but sometimes you need information that tools can actually update as a conversation unfolds. That's custom state, and it's the last piece needed before Aria can handle real authentication.
References
- LangChain Academy: Introduction to LangChain (Python) — this section is inspired by and adapted from this course
- LangChain Docs: Context — official guide to runtime context and
context_schema - Python Docs:
dataclasses— official reference for thedataclassdecorator langchainon PyPI — latest version and release history