The OpenAI Agents SDK can pause a tool call before it runs. Actionbox gives that interruption a durable place for a person to review it from the web, mobile app, or CLI. The agent keeps its native RunState; Actionbox returns the typed decision used to approve or reject the exact interruption.
This separation is the simplest reliable OpenAI Agents SDK human-in-the-loop architecture:
What OpenAI Agents owns—and what Actionbox owns
| Concern | Owner |
|---|---|
| Agent conversation and tool interruption | OpenAI Agents SDK |
Paused and resumed RunState | OpenAI Agents SDK |
| Reviewer notification and inbox | Actionbox |
| Typed approval or rejection | Actionbox |
| Actual tool credentials and side effect | Your application |
Actionbox is not a replacement state store for the Agents SDK. If a reviewer may answer after the current worker exits, persist the resumable SDK state in your own durable store before releasing the worker.
Install the SDKs
python -m pip install actionbox-sdk openai-agents
export ACTIONBOX_API_KEY="axb_live_..."
export OPENAI_API_KEY="..."Create an Actionbox Source for the integration and keep both keys in your secret manager. Never place either key in the Action title, description, context, or logs.
Complete OpenAI Agents tool approval example
Mark only consequential tools with needs_approval=True. Safe read-only tools can continue without consuming human attention.
from __future__ import annotations
import asyncio
import hashlib
import os
from actionbox import Actionbox
from agents import Agent, Runner
from agents.decorators import tool
@tool(needs_approval=True)
async def deploy_service(environment: str, version: str) -> str:
"""Deploy one service version after approval."""
# Replace this demo return with the real side effect after testing.
return f"Deployed {version} to {environment}"
async def ask_actionbox(client: Actionbox, item) -> bool:
call_id = getattr(item, "call_id", None)
if not isinstance(call_id, str) or not call_id:
raise ValueError("A stable tool-call ID is required")
tool_name = (
getattr(item, "qualified_name", None)
or getattr(item, "name", None)
or "unknown_tool"
)
arguments = getattr(item, "arguments", None) or "{}"
identity = hashlib.sha256(call_id.encode()).hexdigest()
action = await asyncio.to_thread(
client.create,
title=f"Approve {tool_name}"[:200],
description="The OpenAI Agents SDK paused this exact tool call.",
interaction={
"type": "boolean",
"label": "Allow this tool call?",
"true_label": "Approve",
"false_label": "Reject",
},
context=[{
"type": "code",
"title": "Arguments — remove secrets before sending",
"language": "json",
"content": arguments,
}],
metadata={
"framework": "openai_agents",
"tool_name": tool_name,
"tool_call_id": call_id,
},
idempotency_key=f"openai-agents:{identity}",
)
response = await asyncio.to_thread(action.wait, timeout=3600)
if response is None and action.status == "open":
await asyncio.to_thread(
client.cancel,
action.id,
"OpenAI Agents approval timed out; rejected fail-closed.",
)
return (
isinstance(response, dict)
and response.get("type") == "boolean"
and response.get("value") is True
)
async def main() -> None:
agent = Agent(
name="release-agent",
instructions="Deploy version 2.18.0 to staging using the tool.",
tools=[deploy_service],
)
with Actionbox(os.environ["ACTIONBOX_API_KEY"]) as actionbox:
result = await Runner.run(agent, "Deploy the requested release.")
while result.interruptions:
state = result.to_state()
decisions = await asyncio.gather(
*(ask_actionbox(actionbox, item) for item in result.interruptions)
)
for item, approved in zip(
result.interruptions, decisions, strict=True
):
if approved:
state.approve(item, always_approve=False)
else:
state.reject(
item,
rejection_message="Rejected or timed out in Actionbox.",
)
result = await Runner.run(agent, state)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())The repository contains the maintained runnable version at integrations/agent_frameworks/openai_agents_hitl.py. It also rejects oversized approval context instead of silently showing the reviewer an incomplete payload.
Why result.to_state() matters
The interruption belongs to the run that produced it. Convert that result to its resumable state, apply each decision to that state, and pass the state back to Runner.run. Starting a fresh run after approval can lose the exact tool-call identity and conversation state the person reviewed.
Use always_approve=False for a one-time decision. A reviewer approving one deployment should not silently authorize every future call to the same tool.
Handle every interruption
A run can surface more than one approval item. Route every item, then approve or reject each exact object on the resumable state. The example gathers independent human requests concurrently so one reviewer decision does not prevent the others from being delivered.
Use the stable tool-call ID in the Actionbox idempotency key. A worker retry for the same interruption should recover the same logical Action instead of creating another notification.
Fail closed on timeout or malformed responses
A boolean Action returns a typed envelope such as {"type":"boolean","value":true}. Do not treat a truthy string, a missing response, or a local timeout as approval.
If the local wait expires, cancel the still-open Action when that run will no longer accept the answer. This prevents a late approval from looking valid after the worker has already rejected or abandoned the interruption.
Production checklist
- Persist the resumable Agents SDK state when the wait can outlive the worker.
- Redact tokens, credentials, personal data, and secret-bearing arguments.
- Reject payloads that cannot be shown completely within your context limit.
- Bind retries to the stable tool-call ID.
- Approve the exact interruption with
always_approve=False. - Reject expiry, timeout, and malformed typed responses.
- Record the real execution outcome after an approved tool finishes.
For the framework-neutral architecture, read human-in-the-loop AI agents. If your application uses checkpointed graphs instead, follow the LangGraph human-in-the-loop tutorial. For the resulting evidence chain, see AI agent audit trails.
Create a free Source · Human approval API · Read the integration docs
Sources and further reading
- OpenAI Agents SDK human-in-the-loop documentation —
needs_approval, interruptions,RunState, and resume. - Actionbox documentation — typed interactions, bounded context, idempotency, and outcomes.
- LangGraph human-in-the-loop with Actionbox — the equivalent checkpoint-based integration.



