# OpenAI Agents SDK

Source: https://pushary.com/docs/agents/build/openai-agents
Last updated: 2026-10-04

> OpenAI Agents SDK human in the loop approvals. Answer needsApproval interruptions from your user's phone, in TypeScript or Python. No answer means no.

The OpenAI Agents SDK has human in the loop approvals built in: mark a tool with `needsApproval` and the run stops with `interruptions` before the tool executes. Somebody still has to decide. `@pushary/openai-agents` (TypeScript) and `pushary-openai-agents` (Python) send each interruption to your user's phone, record their answer on the run state, and hand it back so you can continue.

<Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
  <Tab value="TypeScript">
    ```bash
    npm i @pushary/openai-agents @openai/agents zod
    export PUSHARY_API_KEY=pk_xxx.xxx
    ```
  </Tab>

  <Tab value="Python">
    ```bash
    pip install pushary-openai-agents
    export PUSHARY_API_KEY=pk_xxx.xxx
    ```
  </Tab>
</Tabs>

Get the key at [Partner onboarding](https://pushary.com/onboarding/partner). The TypeScript package needs Node.js 22 or later. The Python package installs `openai-agents` 0.18 or later for you.

## Set up OpenAI Agents SDK human in the loop [#set-up-openai-agents-sdk-human-in-the-loop]

<Steps>
  <Step>
    ### Connect a user once [#connect-a-user-once]

    <Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
      <Tab value="TypeScript">
        ```ts
        import { connect } from '@pushary/openai-agents'

        const { universalLink } = await connect({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)
        ```
      </Tab>

      <Tab value="Python">
        ```python
        from pushary_openai_agents import connect

        link = connect(user.id)
        ```
      </Tab>
    </Tabs>

    The user opens the link on their phone once. They install the free Pushary app and never sign up or pay.
  </Step>

  <Step>
    ### Mark the tool and resolve its interruptions [#mark-the-tool-and-resolve-its-interruptions]

    <Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
      <Tab value="TypeScript">
        ```ts
        import { Agent, run, tool } from '@openai/agents'
        import { z } from 'zod'
        import { pusharyNeedsApproval, resolvePusharyInterruptions } from '@pushary/openai-agents'

        const issueRefund = tool({
          name: 'issue_refund',
          description: 'Refund an order',
          parameters: z.object({ orderId: z.string(), amount: z.number() }),
          needsApproval: pusharyNeedsApproval(),
          execute: async ({ orderId, amount }) => refundOrder(orderId, amount),
        })

        const agent = new Agent({
          name: 'Refunds',
          instructions: 'Refund orders when the customer asks.',
          tools: [issueRefund],
        })

        let result = await run(agent, 'Refund order 1234 in full.')
        while (result.interruptions?.length) {
          const outcome = await resolvePusharyInterruptions(
            { externalId: user.id },
            { interruptions: result.interruptions, state: result.state },
          )
          result = await run(agent, result.state)
          if (!outcome.allApproved) break
        }
        ```
      </Tab>

      <Tab value="Python">
        ```python
        import asyncio

        from agents import Agent, Runner, function_tool
        from pushary_openai_agents import pushary_needs_approval, resolve_pushary_interruptions

        @function_tool(needs_approval=pushary_needs_approval())
        def issue_refund(order_id: str, amount: float) -> str:
            return refund_order(order_id, amount)

        agent = Agent(name="Refunds", instructions="Refund orders when the customer asks.", tools=[issue_refund])

        async def main() -> None:
            result = await Runner.run(agent, "Refund order 1234 in full.")
            while result.interruptions:
                outcome = resolve_pushary_interruptions(result, external_id=user.id)
                result = await Runner.run(agent, outcome.state)
                if not outcome.all_approved:
                    break

        asyncio.run(main())
        ```
      </Tab>
    </Tabs>

    `refundOrder` and `refund_order` are your own functions.
  </Step>

  <Step>
    ### Run it and answer on the phone [#run-it-and-answer-on-the-phone]

    The run stops before `issue_refund` executes. Your user's phone buzzes with the tool name and its arguments. They tap Approve, and the tool runs when you continue the run.
  </Step>
</Steps>

Each interruption becomes one question on the phone, one at a time, in order.

## What happens when nobody answers [#what-happens-when-nobody-answers]

The tool does not run. Each question waits up to 55 seconds. A deny or no answer in time rejects the interruption, and the model gets the reason as the rejection message so it does not keep retrying:

| Outcome                | Rejection message the model reads                                                |
| ---------------------- | -------------------------------------------------------------------------------- |
| Your user taps Deny    | `The approver denied this action.`                                               |
| Nobody answers in time | `No answer from the approver, so this was denied. Do not retry the same action.` |

`allApproved` (`all_approved` in Python) is then false. The loop continues the run once, so the model reads the reason, and then stops.

## Let the model ask a question [#let-the-model-ask-a-question]

<Tabs items="[&#x22;TypeScript&#x22;, &#x22;Python&#x22;]">
  <Tab value="TypeScript">
    ```ts
    import { Agent, run } from '@openai/agents'
    import { pusharyTool } from '@pushary/openai-agents'

    const agent = new Agent({
      name: 'Support',
      instructions: 'Ask the customer which order they need help with.',
      tools: [pusharyTool({ apiKey: process.env.PUSHARY_API_KEY! }, { externalId: user.id })],
    })

    const result = await run(agent, 'Which order needs help?')
    ```
  </Tab>

  <Tab value="Python">
    ```python
    from agents import Agent, Runner
    from pushary_openai_agents import pushary_tool

    agent = Agent(
        name="Support",
        instructions="Ask the customer which order they need help with.",
        tools=[pushary_tool(user.id)],
    )
    result = Runner.run_sync(agent, "Which order needs help?")
    ```
  </Tab>
</Tabs>

The model decides when to call this tool, so it suits questions. Keep `needsApproval` for anything that must not happen without a yes.

## Production notes [#production-notes]

### Make sure a person always decides [#make-sure-a-person-always-decides]

The resolver asks your site's policy first. A policy rule that names the tool can allow or deny it without paging anyone. Pass `policy: false` (Python: `policy=False`) when a person must always answer.

### Continue from the saved state [#continue-from-the-saved-state]

In Python, continue with `outcome.state`. It carries the answers. Continuing from `result.to_input_list()` replays the conversation without them, so the model asks for the same tool again and your user gets asked twice.

### Retries and idempotency [#retries-and-idempotency]

Each question is keyed on the tool call id, the recipient and the arguments, so an exact retry reuses it. Changed arguments or a changed recipient ask again. If you replay a run under ids you mint yourself, pass a stable `runId` (Python: `run_id`).

### Pick the right person [#pick-the-right-person]

The user id is bound in your code, never taken from the model, so a tricked model cannot ask the wrong person. For a multi-tenant agent, pass a function as `externalId` that picks the person for each interruption from your own records.

### Answers that come later than 55 seconds [#answers-that-come-later-than-55-seconds]

The resolver waits inside one request. For answers that may take hours, save the run state with the decision id, customer and exact call arguments, then resume only that call after you read the answer. The [TypeScript delayed-answer recipe](https://github.com/Pushary/pushary-openai-agents/blob/main/examples/DELAYED-REVIEWS.md) does this with SQLite and needs Node.js 22.13 or later. In Python you build the same coordination around the saved state. To wake your worker when an answer lands, use a [signed webhook](/docs/agents/embed#webhooks).

### Versions [#versions]

`@pushary/openai-agents` 0.4 and `pushary-openai-agents` 0.4 require the shared SDK 2.1. Finish pending runs on their old version before you upgrade.

Source and more examples: [pushary-openai-agents on GitHub](https://github.com/Pushary/pushary-openai-agents). OpenAI reference: [human in the loop in TypeScript](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/) and [in Python](https://openai.github.io/openai-agents-python/human_in_the_loop/).

## Next steps [#next-steps]

<Cards>
  <Card title="Use the hosted Agents API instead" description="Sessions kept by OpenAI: save the pending call, ask, then submit the tool result." href="/docs/agents/build/hosted-openai-agents" />

  <Card title="See the OpenAI Agents SDK approval workflow" description="The whole flow for OpenAI Agents SDK agents, from connect to resume." href="/human-in-the-loop-openai-agents-sdk" />

  <Card title="Try a simulated decision" description="Approve or deny a refund in your browser before you connect a phone." href="/try" />
</Cards>