Human-in-the-loop for the OpenAI Agents SDK

Pause the tool. Get customer approval on a phone.

Your OpenAI agent stops before a refund. Your user approves it on their phone, and the run carries on.

What OpenAI Agents SDK gives you

needsApproval and run interruptions

Mark a function tool with needsApproval and the SDK stops before running it. The run returns result.interruptions and a RunState you can save. Your code approves or rejects each interruption, then runs the agent again from that state.

Where it stops

  • needsApproval decides that a person is needed. Nothing asks anyone; resolving the interruption is your job.
  • A long wait means saving RunState and restoring it with the same agent and SDK version.
  • It does not know which of your users should answer, or how to reach them.

The fix

Pushary gets your user's answer.

The Agents SDK can pause a tool call, but it tells nobody. Pushary sends each paused call to your user's phone and puts the answer back on the run.

issue_refund needs approval, so the run stops before it executes. resolvePusharyInterruptions asks your user about each paused call, one at a time. Approve runs the refund. Deny, expiry or no answer rejects it, and the model gets the reason. If one of your Pushary rules covers issue_refund, the rule answers first.

bash
npm i @pushary/openai-agents @openai/agents zod
approval on the risky tool
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({ amount: z.number() }),
  needsApproval: pusharyNeedsApproval(),
  execute: async ({ amount }) => chargeBack(amount),
})

const agent = new Agent({ name: "Refunds", instructions: "Refund orders when asked.", tools: [issueRefund] })

let result = await run(agent, "Refund order 1234")
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
}

Streaming and the Responses API save different things

For a streamed run, read the events and await result.completed before you check interruptions and save the state. The separate Responses API recipe saves the response id and the exact MCP approval request, then sends mcp_approval_response with previous_response_id. The tutorial covers both, with real services.

settle the stream before saving
const result = await runner.run(agent, input, { stream: true });
for await (const event of result) {
  // Display appropriate events; output is not permission to act.
}
await result.completed;
// Now inspect result.interruptions and persist result.state.
// The complete recipe binds this state to customer + action.

How it works

From install to the first answer

Connect your user once. Put the approval on the risky step. Pushary sends the question, waits for the answer and saves it.

  1. 01

    Install the adapter

    Run npm i @pushary/openai-agents @openai/agents zod and put your API key in PUSHARY_API_KEY. For Python, pip install pushary-openai-agents.

  2. 02

    Connect your user once

    connect() returns a link. Your user opens it on their phone with the free Pushary app. Keep your API key and their user id on your server.

  3. 03

    Put the approval on the risky tool

    Set needsApproval: pusharyNeedsApproval() on the tool. The run stops before it executes, and resolvePusharyInterruptions sends each paused call to your user.

  4. 04

    Act on the answer

    On Approve, run the agent again from result.state. On Deny or no answer the call is rejected. For long waits, save the state and resume when the answer arrives.

A question can be a yes or no, a choice from a list, or a typed answer. Every answer is saved.

Before you ship

Things that bite on OpenAI Agents SDK

An ask tool the model can skip is not a gate
pusharyTool is a tool the model chooses to call. That suits “go ask about this”. For “this must not happen without a yes”, put needsApproval on the risky tool itself. A prompt that says “ask first” does not stop anything.
A saved answer is not a saved agent
Save the run state, the exact call, the user and the action version before you ask. Pushary keeps the answer, but it does not rebuild your process or schedule the resume. That runs in your own job system.
Approval covers the call your user saw
Read the answer from Pushary again and check it matches the saved call. Only a yes to a yes-or-no question lets the call run. If the draft changes, ask again. Waiting, expired, cancelled and denied requests never count.
A retry can still run the action twice
The example runs each resume only once, but your refund API still needs its own operation key so a repeat does nothing. If a network result is unclear, check what happened before you retry. The example handles one paused call per saved operation.

The Partner plan

Your agent asks your users, not just you

Ship your agent to your users. Each of them approves its risky actions from their phone, under your brand.

$99/ month, every framework included
Start free

Your users never make a Pushary account or pay us.

Your users answer on their own phones

Connect each user once with a link. They install the free Pushary app and answer from the lock screen, with no account of their own.

No answer means no

Run the action only after an explicit Approve. A request that is still waiting, expired or cancelled never counts as a yes. Pushary keeps every request, so your code can pick it up again after a restart.

Reaches them on their phone

The app comes first, because only the app puts Approve and Deny on the lock screen. Browser notifications come next. You build no delivery system.

Every answer saved

Every answer is saved with who answered and when. You can search and export it, and with a callback URL, each answer is also sent to your server as a signed webhook.

FAQ

Questions, answered

Which OpenAI packages are available?
@pushary/openai-agents for TypeScript and pushary-openai-agents for Python. Both give you connect(), an ask tool, and an approval gate for needsApproval (pusharyNeedsApproval and resolvePusharyInterruptions, or the snake_case names in Python). The saved-state, streaming and Responses API recipes are TypeScript examples.
Does the streaming example call a real model or phone?
No. It runs the real Agents SDK with simulated model events, HTTP responses and refunds, using SQLite and fresh workers. No account or API key is needed. The tutorial shows how to connect real services; these checks do not prove a phone received a notification.
How does the Responses API MCP example resume?
It saves a completed response id and the exact mcp_approval_request, checks your user's answer, then sends mcp_approval_response with previous_response_id. It keeps require_approval set to always. This is separate from Agents SDK RunState and needs stored responses.
What happens if the customer never answers?
The call does not run. A request that is still waiting is not a yes, and the worker rejects expired, cancelled or denied requests. Your app polls or waits for a signed webhook, and saves the SDK's run state itself, because Pushary keeps the request, not your process.
Does Pushary replace authorization or Codex Auto-review?
No. Your user's answer sits on top of your own authorization, input checks and limits on what each tool can do. Apps built on the Responses API or the Agents SDK do not get Codex Auto-review automatically.

Let your OpenAI Agents SDK agent ask before it acts.

Connect a user once, ask with one call, and run the action only on a yes. Every answer is saved.

Start free

Partner plan, $99 a month. Your users never make a Pushary account or pay us.