← Blog

OpenAI Agents SDK
approval workflow.

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 needsApproval alone 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.

$ npx nominee-cli proof runs offline after npx installs the package
Star on GitHub Read the approval docs →