OpenAI Agents SDK human in the loop for paid tools
Set needs_approval on a function tool that starts a paid video job. The run pauses with result.interruptions until you approve or reject the call.

In the OpenAI Agents SDK, set needs_approval=True on the tool that starts a paid video job. When the model calls it, the run stops and result.interruptions lists the pending call; you turn the result into a RunState, call state.approve(...) or state.reject(...), and resume with Runner.run(agent, state).
The Agents SDK facts come from its Human-in-the-loop and MCP pages, read 2026-09-29. The Sume facts come from Video Generation. Sume has no connector for the Agents SDK; the tool below is a plain HTTPS call. For the MCP route and its timeout, see OpenAI Agents SDK MCP server: Sume and the 5-second timeout.
How do I gate one tool?
The docs say needs_approval accepts True to always require approval, or an async function that decides per call from the run context, the parsed parameters, and the tool call ID. Callable rules fail closed: if the arguments are missing, empty, malformed JSON, or not a JSON object, the callable is not invoked and the call requires manual approval.
The sketch below gates the submit and hashes the prompt into the Idempotency-Key, so the same prompt sent twice replays the first job on Sume's side (same key, same body). Change the key when you want a genuinely new job.
import asyncio, hashlib, os, requests
from agents import Agent, Runner
from agents.decorators import tool
@tool(needs_approval=True)
async def start_video(prompt: str) -> str:
"""Start a paid Sume video job and return its job id."""
key = "oai-" + hashlib.sha256(prompt.encode()).hexdigest()[:16]
r = await asyncio.to_thread(
requests.post, "https://api.sume.com/v1/videos", timeout=30,
json={"model": "sume/auto", "prompt": prompt},
headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
"Idempotency-Key": key})
r.raise_for_status()
return r.json()["id"]
async def main():
agent = Agent(name="Video agent", instructions="Use start_video.", tools=[start_video])
result = await Runner.run(agent, "Make a 5-second clip of a desk lamp.")
while result.interruptions:
state = result.to_state()
for item in result.interruptions:
ok = input(f"Approve {item.name} {item.arguments}? [y/N] ").lower() == "y"
state.approve(item, always_approve=False) if ok else state.reject(item)
result = await Runner.run(agent, state)
print(result.final_output)
asyncio.run(main())Which tool types can pause a run?
The Agents SDK has more than one approval switch, depending on where the tool lives.
| Tool type | How approval is set |
|---|---|
Function tool, Agent.as_tool | needs_approval as True or a callable |
Local MCP servers (MCPServerStdio, MCPServerSse, MCPServerStreamableHttp) | require_approval |
HostedMCPTool | tool_config={"require_approval": "always"} plus an optional on_approval_request callback |
Local ShellTool, ApplyPatchTool | needs_approval, off by default; on_approval can decide in code |
Should I use always_approve for a paid tool?
Usually not. By default an approval is scoped to the specific call ID. always_approve=True persists the decision for future calls to the same tool for the rest of the run, and the docs say those sticky decisions survive to_string() and from_string() when you resume a paused run later. For a tool that spends balance on every call, per-call approval is the setting that matches the intent. For hosted MCP tools the docs key a sticky decision on server_label plus tool name, so a same-named tool on another server is not approved by it.
What does the model see after a rejection?
By default the SDK's standard rejection text goes back into the run. You can set RunConfig.tool_error_formatter for a run-wide message, or pass rejection_message= to state.reject(...) for one call; the per-call message wins. A clear message such as "Video was not started because approval was rejected" gives the model less reason to retry the same call.
What if I use Sume's hosted MCP instead?
The same page covers hosted MCP: set require_approval to "always" on HostedMCPTool. Sume's MCP docs say an API-key session sees the whole tool set and that a paid tool's idempotency_key is "for transport/dedup, not human approval" (see MCP tools and gates), so the approval has to come from your Agents SDK configuration.
Sources
Related posts
More in Integrations
- p-retry npm: retry a paid API POST and stop on errors
p-retry reruns an async function with exponential backoff. Throw AbortError on answers a resend can't fix, and keep one Idempotency-Key per job.
- PHP cURL POST JSON with a Bearer token
json_encode the body, pass the string to CURLOPT_POSTFIELDS, set Content-Type and Authorization headers, then check the status: cURL won't fail on a 4xx.
- Polly retry policy for an HttpClient POST to a paid API
A Polly retry for a paid POST: handle only transient failures, back off exponentially with jitter, honor Retry-After, and resend one idempotency key.
- Power Automate HTTP Webhook action: wait for a callback
The HTTP Webhook action sends a subscribe request with the flow's callback URL, then pauses until something POSTs to it. How to use it with a slow API.
Written by Sume