0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

Passing data with RunContextWrapper

RunContextWrapper is a wrapper the SDK hands to your tools that carries the object you passed to the run: a tool declares ctx: RunContextWrapper[Customer] and reads ctx.context.

Last updated: 28 Sep, 2026 · openai-agents 0.22.3

The model should never be trusted to supply who the caller is. You know the customer before the run starts, so you pass it in as context and let the tool read it, instead of hoping the model fills a tool argument right.

A tool that takes RunContextWrapper

Make the first parameter of the tool ctx: RunContextWrapper[T]. The SDK fills it in for you, and ctx.context is the object you gave to the run. This first parameter is not part of the tool's schema, so the model never sees it or fills it.

python
@function_tool
def my_order(ctx: RunContextWrapper[Customer]) -> str:  # first param is context
    return ctx.context.order_id   # the caller data, not a model argument

The context object

Any object works. A dataclass is a small, clear one. It holds what you know about the caller before the run.

python
from dataclasses import dataclass

@dataclass
class Customer:
    name: str       # who is calling
    order_id: str   # their order, known before the run

A tool that reads the caller

The tool takes no order id from the model. It reads the id from the context, so it always answers about the caller, not about whatever the model typed.

python
@function_tool
def my_order(ctx: RunContextWrapper[Customer]) -> str:
    "Look up the calling customer's own order."
    c = ctx.context                      # the object you passed to the run
    return f"{c.name}'s order {c.order_id}: shipped on 3 March."

Passing context into the run

Pass the object as context= on the run. It reaches the tool through ctx.context. Here ToolModel is a stand-in that calls the tool once, then returns the tool's output as the reply; the full program shows it.

python
agent = Agent(name="Desk", instructions="Help with the caller's order.",
              tools=[my_order], model=ToolModel())
# context=... reaches every tool through ctx.context
Runner.run_sync(agent, "where is my order?", context=Customer("Ada", "A17"))

Answering two callers from context

The same agent and the same question run twice, with a different Customer each time. The reply changes because the tool read the context, not a model argument.

Example
from dataclasses import dataclass
from agents import Agent, Runner, function_tool, RunContextWrapper, set_tracing_disabled
from agents.models.interface import Model
from agents.items import ModelResponse
from agents.usage import Usage
from openai.types.responses import (
    ResponseOutputMessage, ResponseOutputText, ResponseFunctionToolCall,
)
set_tracing_disabled(True)


@dataclass
class Customer:
    name: str
    order_id: str


@function_tool
def my_order(ctx: RunContextWrapper[Customer]) -> str:
    "Look up the calling customer's own order."
    c = ctx.context
    return f"{c.name}'s order {c.order_id}: shipped on 3 March."


def _message(text):
    return ResponseOutputMessage(
        id="msg", role="assistant", type="message", status="completed",
        content=[ResponseOutputText(text=text, type="output_text", annotations=[])],
    )


def _tool_call(name, arguments):
    return ResponseFunctionToolCall(
        id="fc", call_id="call_1", name=name, arguments=arguments, type="function_call",
    )


class ToolModel(Model):
    async def get_response(self, system_instructions, input, model_settings, tools,
                           output_schema, handoffs, tracing, **k):
        if isinstance(input, list):
            for it in reversed(input):
                d = it if isinstance(it, dict) else it.__dict__
                if d.get("type") == "function_call_output":
                    return ModelResponse(output=[_message(d.get("output", ""))],
                                         usage=Usage(), response_id=None)
        return ModelResponse(output=[_tool_call("my_order", "{}")],
                             usage=Usage(), response_id=None)

    async def stream_response(self, *a, **k):
        raise NotImplementedError


agent = Agent(name="Desk", instructions="Help with the caller's order.",
              tools=[my_order], model=ToolModel())

print(Runner.run_sync(agent, "where is my order?",
                      context=Customer("Ada", "A17")).final_output)
print(Runner.run_sync(agent, "where is my order?",
                      context=Customer("Sam", "B92")).final_output)

Why the two replies differ

  • The id came from context: the tool never took an order_id argument, so the model could not change it.
  • Ada saw A17, Sam saw B92: only context= differed between the two runs.
  • ctx is not in the schema: the model was asked to call my_order with no arguments, and it did.

Context data vs a tool argument

Tool argumentContext (ctx.context)
Who supplies itthe modelyour code, before the run
In the tool schemayesno
Can the model change ityesno
Good forwhat the model decideswho the caller is, secrets, handles

When to reach for context

  • Identity: the current user or customer, so a tool acts as them.
  • Handles: a database connection or an API client a tool needs but the model should not see.
Watch out. The context type is yours, not the model's. If a tool puts ctx anywhere but the first parameter, or leaves off the type, the SDK cannot fill it and the call fails.
Try it yourself
  • Add an email: str field to Customer and return it from the tool.
  • Run a third time with Customer("Lee", "C33") and read the reply.
  • Remove ctx from the tool signature and read the error the run gives.

Slow is fine. Stopping is the only problem.