---
title: "OpenAI Agents SDK"
description: "The OpenAI Agents SDK defines an agent with instructions, function tools, and handoffs. The Dex adapter runs the SDK's runner inside a durable session. It is available for Python and TypeScript."
canonical_url: "https://docs.superdurable.io/agent-frameworks/openai-agents-sdk/"
source: "docs/content/agent-frameworks/openai-agents-sdk.mdx"
locale: "en"
---

# OpenAI Agents SDK

The OpenAI Agents SDK defines an agent with instructions, function tools, and handoffs. The Dex adapter runs the SDK's runner inside a durable session. It is available for Python and TypeScript.

## When to use it

Use it when your agent already uses the OpenAI Agents SDK, with or without handoffs. Keep the SDK's agent definition, tools, and guardrails, and let Dex own the session.

## How a session runs

The adapter copies the agent graph once and intercepts every function tool. So the SDK stops right after each model call and returns the tool calls the model asks for. Each **RunModel** Step makes one model call.

Each function tool call runs in its own Step, in parallel: **CallTool** for a read or an idempotent write, **CallSingleAttemptTool** for any other write. A call that needs approval waits in **AwaitToolApproval** first. The next **RunModel** restores the SDK's run state and gives it the recorded results. The tool does not run again.

A handoff is not a function tool. It runs inside **RunModel**, so a Step that hands off also makes the next agent's model call.

A turn that looks up an order runs **RunModel**, **CallTool**, **ResumeTurn**, and **RunModel**.

## Install

Install the adapter with its framework. Each adapter brings in the Dex Agents core.

**Python**

```bash
pip install dex-agents-openai-agents
```

Example: [agents/python/openai-agents/README.md](https://github.com/superdurable/dex/tree/main/agents/python/openai-agents/README.md)

**TypeScript**

```bash
npm i @superdurable/dex-agents-openai-agents @superdurable/dex-agents @superdurable/dex @openai/agents zod
```

Example: [agents/typescript/openai-agents/README.md](https://github.com/superdurable/dex/tree/main/agents/typescript/openai-agents/README.md)

## The example

The example is the [order support agent](https://docs.superdurable.io/agent-frameworks.md#the-examples) that every framework page uses.

The Flow type is **OpenAIAgentsOrderSupport** in Python and **OpenAIAgentsOrderSupportAgent** in TypeScript. The full example is under [examples/python/agent-harnesses](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses) and [examples/typescript/src/products/agent-harnesses](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses).

### Agent and tools

The tools are function tools of the SDK. The agent is a plain SDK agent. The tool policies name each tool.

**Python**

```python
def build_openai_agents_order_support_flow(
    order_desk: OrderDesk, model: str | Model, slack_connection: slack.Connection | None
) -> AgentSessionFlow:
    """Builds the session Flow; ``model`` is an OpenAI model name, such as "gpt-5.2"."""

    # The tools are closures, so they call the OrderDesk passed in here.
    @function_tool
    def look_up_order(order_id: str) -> str:
        """Look up one order by its order ID and return its items and status."""
        return order_desk.describe_order(order_id)

    @function_tool
    def cancel_order(order_id: str, reason: str) -> str:
        """Cancel one order that has not shipped yet."""
        return order_desk.cancel_order(order_id, reason)

    agent = Agent(
        name="OrderSupport",
        instructions=ORDER_SUPPORT_INSTRUCTIONS,
        model=model,
        tools=[look_up_order, cancel_order],
    )
    return openai_agents_session_flow(
        agent,
        flow_type=OPENAI_AGENTS_ORDER_SUPPORT_FLOW_TYPE,
        tool_sources=build_slack_tool_sources(slack_connection),
        tool_policies={
            "look_up_order": ToolPolicy(ToolEffect.READ),
            "cancel_order": ToolPolicy(ToolEffect.NON_IDEMPOTENT_WRITE),
        },
    )
```

Example: [examples/python/agent-harnesses/agent_harness_examples/openai_agents_order_support.py](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/agent_harness_examples/openai_agents_order_support.py)

**TypeScript**

```typescript
export function createOpenAIAgentsOrderSupportFlow(
  orderDesk: OrderDesk,
  model: string | Model,
  slack: Connection | undefined,
): AgentSessionFlow {
  const lookUpOrder = tool({
    name: "look_up_order",
    description: "Look up one order by its order ID and return its items and status.",
    parameters: z.object({ order_id: z.string() }),
    // Without errorFunction: null, tool() turns every thrown error into model-visible text.
    errorFunction: null,
    execute: ({ order_id }) => orderDesk.describeOrder(order_id),
  });
  const cancelOrder = tool({
    name: "cancel_order",
    description: "Cancel one order that has not shipped yet.",
    parameters: z.object({ order_id: z.string(), reason: z.string() }),
    errorFunction: null,
    execute: ({ order_id, reason }) => orderDesk.cancelOrder(order_id, reason),
  });
  const agent = new Agent({
    name: "OrderSupport",
    instructions: orderSupportInstructions,
    model,
    tools: [lookUpOrder, cancelOrder],
  });
  return createOpenAIAgentsSessionFlow(agent, openAIAgentsOrderSupportFlowType, {
    toolPolicies: {
      look_up_order: { effect: "read" },
      cancel_order: { effect: "non_idempotent_write" },
    },
    toolSources: createSlackConnectorToolSources(slack),
  });
}
```

Example: [examples/typescript/src/products/agent-harnesses/openai-agents/openai-agents-order-support-flow.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses/openai-agents/openai-agents-order-support-flow.ts)

### Worker

One Worker serves the session Flows of every harness in the example. Agent session Steps are async, so the Python Registry allows async handlers. Build the same session Flow in every process that starts sessions or calls their RPCs.

**Python**

```python
        # Agent session Steps are async, so the Registry allows async handlers.
        self.registry = Registry(
            tuple(example.session_flow for example in self.examples), allow_async_handlers=True
        )
        config.blob_cache_dir.mkdir(parents=True, exist_ok=True)
        self.blob_cache = open_blob_cache(BlobCacheConfig(str(config.blob_cache_dir), 1 << 30))
        self.worker = AsyncWorker(
            self.registry,
            self.blob_cache,
            WorkerOptions(
                server_address=config.server_address,
                pull=PullWorkerOptions(max_concurrent_tasks=MAX_CONCURRENT_TASKS),
            ),
        )
        self.client = AsyncClient(
            self.registry, self.blob_cache, ClientOptions(server_address=config.server_address)
        )

# ...

def _new_openai_agents_example(
    config: AgentHarnessExamplesConfig, slack_connection: slack.Connection | None
) -> AgentHarnessExample:
    order_desk = OrderDesk()
    model = _openai_agents_model(config.openai_agents_model)
    flow = build_openai_agents_order_support_flow(order_desk, model, slack_connection)
    return AgentHarnessExample("openai-agents", flow, order_desk)

# ...

def _openai_agents_model(model: str) -> str | Model:
    return OpenAIAgentsDemoModel() if is_demo_model(model) else model
```

Example: [examples/python/agent-harnesses/agent_harness_examples/app.py](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/agent_harness_examples/app.py)

**TypeScript**

```typescript
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
// ...
// One order desk for the four agent harness examples; each harness registers its own session Flow type.
const orderDesk = new OrderDesk();
// ...
const openAIAgentsOrderSupportFlow = createOpenAIAgentsOrderSupportFlow(orderDesk, loadOpenAIAgentsOrderSupportModel(), slack);
```

Example: [examples/typescript/src/registry.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/registry.ts)

### Session routes

Every harness serves these same routes. [Session routes](https://docs.superdurable.io/agent-frameworks.md#session-routes) explains them and the session token.

**Python**

```python
    @blueprint.post("/sessions")
    async def start_session() -> Response:
        # The server picks the session ID, so a caller cannot claim an existing session.
        session_id = f"{harness_name}-order-support-{uuid.uuid4()}"
        await sessions.start_session(session_id)
        session_token = sign_session_id(app_state.config.session_key, session_id)
        return jsonify(session_id=session_id, session_token=session_token)

    @blueprint.post("/messages")
    async def send_user_message() -> Response:
        body, session_id = await session_request()
        accepted = await sessions.send_user_message(session_id, _required_string(body, "text"))
        return jsonify(accepted=accepted)

    @blueprint.post("/interrupts/resolve")
    async def resolve_interrupt() -> Response:
        body, session_id = await session_request()
        interrupt_id = _required_string(body, "interrupt_id")
        if not isinstance(approved := body.get("approved"), bool):
            raise BadRequest("approved must be a boolean")
        rejection_message = body.get("rejection_message")
        if rejection_message is not None and not isinstance(rejection_message, str):
            raise BadRequest("rejection_message must be a string")
        accepted = await sessions.resolve_interrupt(
            session_id, interrupt_id, approved=approved, rejection_message=rejection_message
        )
        return jsonify(accepted=accepted)

    @blueprint.get("/snapshot")
    async def get_agent_snapshot() -> Response:
        session_id = request.args.get("session_id", "")
        if not session_id:
            raise BadRequest("session_id is required")
        authorize_session(session_id)
        try:
            snapshot = await read_agent_event_stream_snapshot(
                app_state.client, example.session_flow, session_id
            )
        except FlowNotActiveOrNotFoundError as error:
            raise NotFound("the agent session is missing or no longer retained") from error
        return jsonify(snapshot.to_json())
```

Example: [examples/python/agent-harnesses/agent_harness_examples/http_routes.py](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/agent_harness_examples/http_routes.py)

**TypeScript**

```typescript
  router.post("/sessions", async (request, response) => {
    const initialMessage = optionalString(requireBody(request), "message");
    // The server picks the session ID, so a caller cannot name another session.
    const sessionId = `order-support-${randomUUID()}`;
    await startAgentSession(client, flow, sessionId, { initialMessage }, { timeoutMs: 30 * DAY_MS });
    response.json({ session_id: sessionId, session_token: issueSessionToken(sessionId) });
  });
  router.get("/sessions/:id/snapshot", async (request, response) => {
    response.json(convertAgentSnapshotToJson(await client.invokeRPC(flow.getAgentSnapshot, sessionIdOf(request))));
  });
  router.post("/sessions/:id/messages", async (request, response) => {
    const sessionId = sessionIdOf(request);
    const message = { text: requireString(requireBody(request), "text") };
    sendAccepted(response, await client.invokeRPC(flow.sendUserMessage, sessionId, message));
  });
  router.post("/sessions/:id/interrupts/:interruptId/approve", async (request, response) => {
    const sessionId = sessionIdOf(request);
    const approval = { interruptId: String(request.params.interruptId), approved: true, rejectionMessage: null };
    sendAccepted(response, await client.invokeRPC(flow.resolveInterrupt, sessionId, approval));
  });
```

Example: [examples/typescript/src/products/agent-harnesses/order-support/order-support-routes.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses/order-support/order-support-routes.ts)

## Run it locally

### Start Dex and the example

In a first terminal, render the session graph, then start Dex. Dex Web reads the graph when it starts, to show the session and its approval controls.

**Python**

```bash
cd examples/python/agent-harnesses
uv sync --locked
mkdir -p definitions
uv run --frozen dex-agents-session-graph --flow-type OpenAIAgentsOrderSupport \
  --permission order-support.operate --out definitions/openai-agents-sdk.json
dexcli dev --flow-rendering-dir ./definitions
```

Example: [examples/python/agent-harnesses/README.md](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/README.md)

**TypeScript**

```bash
cd examples/typescript
npm install
mkdir -p definitions
npx dex-agents-session-graph --flow-type OpenAIAgentsOrderSupportAgent \
  --permission order-support.operate --out definitions/openai-agents-sdk.json
dexcli dev --flow-rendering-dir ./definitions
```

Example: [examples/typescript/src/products/agent-harnesses/README.md](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses/README.md)

**dexcli dev** stays in the foreground. In a second terminal, start the example:

**Python**

```bash
cd examples/python/agent-harnesses
uv run --frozen python -m agent_harness_examples
```

Example: [examples/python/agent-harnesses/README.md](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/README.md)

**TypeScript**

```bash
cd examples/typescript
OPENAI_API_KEY=... npm start
```

Example: [examples/typescript/src/products/agent-harnesses/README.md](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses/README.md)

The Python app serves on **127.0.0.1:8081**. The TypeScript app serves on **127.0.0.1:8080**. Dex Web is at **<http://127.0.0.1:8802>**.

Without **DEX_OPENAI_AGENTS_MODEL**, the Python agent uses **mock/order-support**, a local demo model that needs no key. It calls **look_up_order** for "look up order 1001" and **cancel_order** for "cancel order 1002". The TypeScript example has no demo model, so it needs **OPENAI_API_KEY**.

| Variable                    | Default                                                     | Meaning                                     |
| --------------------------- | ----------------------------------------------------------- | ------------------------------------------- |
| **DEX_OPENAI_AGENTS_MODEL** | Python: **mock/order-support**. TypeScript: **gpt-5-mini**. | The OpenAI model name, such as **gpt-5.2**. |
| **OPENAI_API_KEY**          | none                                                        | The OpenAI API key, for a real model.       |
| **DEX_AGENT_SESSION_KEY**   | a random key                                                | The key that signs session tokens.          |

### Call the session

Start a session, ask the agent to cancel order 1002, and approve the call:

**Python**

```bash
base=http://127.0.0.1:8081/products/agent-harnesses/openai-agents
session=$(curl -s -X POST "$base/sessions" -H 'Content-Type: application/json' -d '{}')
id=$(echo "$session" | jq -r .session_id)
token=$(echo "$session" | jq -r .session_token)
curl -s -X POST "$base/messages" -H 'Content-Type: application/json' \
  -H "X-Agent-Session-Token: $token" -d "{\"session_id\": \"$id\", \"text\": \"Cancel order 1002\"}"
until interrupt=$(curl -s "$base/snapshot?session_id=$id" -H "X-Agent-Session-Token: $token" \
  | jq -re '.snapshot.pending_interrupts[0].interrupt_id'); do sleep 1; done
curl -s -X POST "$base/interrupts/resolve" -H 'Content-Type: application/json' \
  -H "X-Agent-Session-Token: $token" \
  -d "{\"session_id\": \"$id\", \"interrupt_id\": \"$interrupt\", \"approved\": true}"
```

Example: [examples/python/agent-harnesses/README.md](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/README.md)

**TypeScript**

```bash
base=http://127.0.0.1:8080/products/agent-harnesses/openai-agents
session=$(curl -s -X POST "$base/sessions" -H 'Content-Type: application/json' \
  -d '{"message": "Please cancel order 1002"}')
id=$(echo "$session" | jq -r .session_id)
token=$(echo "$session" | jq -r .session_token)
until interrupt=$(curl -s "$base/sessions/$id/snapshot" -H "X-Agent-Session-Token: $token" \
  | jq -re '.pending_interrupts[0].interrupt_id'); do sleep 1; done
curl -s -X POST "$base/sessions/$id/interrupts/$interrupt/approve" -H "X-Agent-Session-Token: $token"
```

Example: [examples/typescript/src/products/agent-harnesses/README.md](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses/README.md)

The loop reads the snapshot until the agent asks to run **cancel_order**. That call waits for approval, then runs once. Read the snapshot again to see the agent's answer.

### Approve in Dex Web

Instead of the last command, you can approve the call in Dex Web. Follow the steps in [Approve in Dex Web](https://docs.superdurable.io/agent-frameworks.md#approve-in-dex-web).

<a id="connect-connectors"></a>

## Connect Connectors

The agent can call [Connector](https://docs.superdurable.io/concepts/connector.md) operations as tools, as [Connectors as tools](https://docs.superdurable.io/agent-frameworks.md#connectors-as-tools) describes. The example adds the Slack tool **slack_post_thread_reply**, which replies in a Slack thread.

<a id="offer-connector-tools"></a>

### Offer the tool

In Python, install the **connectors** extra of **dex-agents** and the Connector package, such as **dex-connector-slack**. In TypeScript, install **@superdurable/connectors-sdk** and the Connector package, such as **@superdurable/connector-slack**. The install commands are in [Connector tools](https://docs.superdurable.io/reference/agents/connector-tools.md#install).

The Worker opens the Slack connection **support-slack** from the project configuration. **connector_tool** in Python and **connectorTool** in TypeScript turns the operation into a tool, and **ConnectorToolSource** holds it. Without the connection, the agent gets no Slack tool.

**Python**

```python
def build_slack_tool_sources(connection: slack.Connection | None) -> list[ToolSource]:
    """Returns the Slack tool source, or no source when Slack is not configured."""
    if connection is None:
        return []
    return [
        ConnectorToolSource([
            # The manifest does not ask for approval; this agent asks before every post.
            connector_tool(
                connection.post_thread_reply(), connection.reference, requires_approval=True
            ),
        ]),
    ]

# ...

async def open_slack_project_connection(
    environment: Mapping[str, str] = os.environ,
) -> SlackProjectConnection | None:
    # ...
    # An incomplete or invalid configuration raises here, so the Worker fails at startup.
    try:
        project = await load_from_environment(environment)
    except FileNotFoundError:
        _log_slack_not_set_up("the connector configuration file does not exist")
        return None
    # ...
    try:
        connection = slack.new_project_connection(project, SLACK_CONNECTION_NAME)
    except BaseException:
        await project.close()
        raise
    return SlackProjectConnection(project, connection)
```

Example: [examples/python/agent-harnesses/agent_harness_examples/slack_connector_tools.py](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/agent_harness_examples/slack_connector_tools.py)

**TypeScript**

```typescript
export async function openSlackConnectionFromProject(
  environment: NodeJS.ProcessEnv = process.env,
): Promise<Connection | undefined> {
  // ...
  const project = await loadFromEnvironment(environment);
  // ...
  return newProjectConnection(project, slackConnectionName);
}

// ...

export function createSlackConnectorToolSources(slack: Connection | undefined): ToolSource[] {
  if (slack === undefined) {
    return [];
  }
  return [
    new ConnectorToolSource([
      connectorTool(slack.operations.postThreadReply(), slack.reference, {
        manifest,
        inputValidator: PostThreadReplyInputSchema,
        // The manifest does not ask for approval; this agent asks before every post.
        requiresApproval: true,
      }),
    ]),
  ];
}
```

Example: [examples/typescript/src/shared/slack-connector-tools.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/shared/slack-connector-tools.ts)

The Worker opens the connection once, at startup, and every agent uses it:

**Python**

```python
async def main() -> None:
    config = AgentHarnessExamplesConfig.from_env()
    slack_setup = await open_slack_project_connection()
    try:
        app_state = AgentHarnessExamplesApp(config, slack_setup.connection if slack_setup else None)
        # ...
    finally:
        if slack_setup is not None:
            await slack_setup.aclose()
```

Example: [examples/python/agent-harnesses/agent_harness_examples/\_\_main\_\_.py](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/agent_harness_examples/__main__.py)

**TypeScript**

```typescript
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
```

Example: [examples/typescript/src/registry.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/registry.ts)

The agent passes the tool source next to its own tools:

**Python**

```python
    return openai_agents_session_flow(
        agent,
        flow_type=OPENAI_AGENTS_ORDER_SUPPORT_FLOW_TYPE,
        tool_sources=build_slack_tool_sources(slack_connection),
        # ...
    )
```

Example: [examples/python/agent-harnesses/agent_harness_examples/openai_agents_order_support.py](https://github.com/superdurable/dex/tree/main/examples/python/agent-harnesses/agent_harness_examples/openai_agents_order_support.py)

**TypeScript**

```typescript
  return createOpenAIAgentsSessionFlow(agent, openAIAgentsOrderSupportFlowType, {
    // ...
    toolSources: createSlackConnectorToolSources(slack),
  });
```

Example: [examples/typescript/src/products/agent-harnesses/openai-agents/openai-agents-order-support-flow.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/agent-harnesses/openai-agents/openai-agents-order-support-flow.ts)

<a id="connector-tool-calls"></a>

### How a call runs

Each call of **slack_post_thread_reply** runs once, in its own **CallSingleAttemptTool** Step, after a person approves it. [The Slack Connector tool](https://docs.superdurable.io/agent-frameworks.md#slack-connector-tool) explains why.

Every agent of the graph offers the Connector tools after its own tools. In Python, the adapter builds a function tool for each tool in the source's cached listing, its **cached_tools()**; **ConnectorToolSource** caches its whole listing. In TypeScript, they are non-strict function tools, and a tool's **needsApproval** and input guardrails do not apply to them. A call never runs inside the SDK; the source decides approval.

<a id="configure-connector-connection"></a>

### Configure the connection

[Connections](https://docs.superdurable.io/reference/agents/connector-tools.md#connector-tools-connection) explains where the Worker finds the **support-slack** connection and what the Slack app needs.

To try it, start the example with **DEX_CONNECTOR_CONFIG_FILE** set. Then ask the agent to reply in a Slack thread, with the channel ID and the thread timestamp:

```text
Reply in Slack thread C0123456789 1710000000.000100: Order 1002 ships on Thursday.
```

The post waits for approval, like **cancel_order**.

## Limits

- Only function tools get their own Steps. Hosted tools, such as web search, run at the provider during the model call. They repeat when **RunModel** reruns.
- Tools the SDK runs itself, such as MCP server tools and local shell tools, cannot wait for approval. The adapter refuses them by default. Wrap a write in a function tool to give it its own Step.
- Function tool names must be unique across the agent graph. The graph is copied when the Flow is built, so later changes to it do not reach the Flow.
- The session is the history. Do not use server-managed conversation state or SDK sessions.
- An agent used as a tool runs its nested run inside one tool Step in Python. TypeScript rejects it; hand off instead.

The adapter README lists every limit. See [Limits for long sessions](https://docs.superdurable.io/reference/agents/session-limits.md) for the server limits.

## Related

- [Agent Frameworks](https://docs.superdurable.io/agent-frameworks.md)
- [Agents reference](https://docs.superdurable.io/reference/agents.md)
- [Python adapter README](https://github.com/superdurable/dex/tree/main/agents/python/openai-agents)
- [TypeScript adapter README](https://github.com/superdurable/dex/tree/main/agents/typescript/openai-agents)
