Short answer: set needsApproval on a function tool, run the agent, resolve every item in result.interruptions with result.state.approve() or reject(), then pass that state back to run. Add application authorization when approval also depends on the signed-in user, a resource, the exact arguments, or policy shared with another runtime.
The current OpenAI Agents SDK human-in-the-loop guide defines this interruption flow. The tool call stays paused until the same RunState carries a decision.
The native workflow is complete for local approval
const closeIssue = tool({
name: 'close_issue',
description: 'Close one GitHub issue',
parameters: z.object({ repo: z.string(), issue: z.number() }),
needsApproval: true,
execute: ({ repo, issue }) => github.closeIssue({ repo, issue }),
})
const agent = new Agent({ name: 'support-agent', tools: [closeIssue] })
let result = await run(agent, 'Close acme/widgets issue 42')
for (const interruption of result.interruptions) {
if (await approvalUi.confirm(interruption)) {
result.state.approve(interruption)
} else {
result.state.reject(interruption)
}
}
if (result.interruptions.length) {
result = await run(agent, result.state)
}
Keep this version when the approval rule belongs to one agent run and the SDK’s serialized state is the record you need. RunState.toString() and RunState.fromString() support approvals that cross requests or process restarts.
A confirmation is still only one input to execution. The approval item does not know whether the current user may close that repository issue, whether their tenant owns it, or whether another framework must apply the same rule.
Bind native approval to application policy
nominee-openai maps a Nominee ask decision to the SDK’s native interruption. On resume, the adapter binds the approved tool-call ID to decision-bound execution:
import { Agent, run } from '@openai/agents'
import { Nominee, allow, ask } from 'nominee'
import { nomineeTool } from 'nominee-openai'
import { z } from 'zod'
const nominee = new Nominee({
policy: {
rules: [allow('github.issue.read'), ask('github.issue.close')],
fallback: 'deny',
},
})
const closeIssue = nomineeTool({
name: 'close_issue',
description: 'Close one GitHub issue',
parameters: z.object({ repo: z.string(), issue: z.number() }),
nominee,
action: 'github.issue.close',
user: session.userId,
resource: ({ input }) => 'repo:' + input.repo + '#' + input.issue,
execute: ({ repo, issue }) => github.closeIssue({ repo, issue }),
})
const agent = new Agent({ name: 'support-agent', tools: [closeIssue] })
const result = await run(agent, 'Close acme/widgets issue 42')
The application still handles result.interruptions with the SDK workflow above. If policy returns deny, the underlying function never starts. If it returns ask, the adapter uses OpenAI’s resumable approval surface and verifies the approved call ID before execution.
Separate confirmation from permission
- Use
needsApprovalalone when one OpenAI agent owns the rule and the proposed call only needs a human confirmation. - Add action policy when execution depends on identity, resource ownership, tenant, exact input, budgets, or rules shared across runtimes.
For the architectural distinction, read the AI SDK can pause a tool call; it cannot define your application policy. The same boundary applies to OpenAI’s native interruption mechanism.
See the boundary run
Approve one tool call and inspect the receipt chain.