> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenAI Agents SDK tool approval

> Route a refund's approval through the OpenAI Agents SDK's own tool-approval interruption.

An Agents SDK agent has a refund tool. When the policy says a human must approve, the run
should stop with the SDK's own `ToolApprovalItem`, where this SDK's users already answer, and
a refusal must reach your code as an exception rather than the model as text to retry.

## The policy

```yaml runnable theme={null}
schema: ctrlrun.policy/v2

actions:
  stripe.refund:
    effect: "refund:{payment_id}"
    rules:
      - when: { amount_gte: 0, amount_lte: 50000 }
        decision: allow
      - decision: approve
```

## The code

This is the adapter's own example. It needs `pip install ctrlrun-openai-agents` and the SDK,
which the harness that runs the other recipes does not have; the adapter's tests run this shape
against a real `openai-agents` in this repository's CI.

```python theme={null}
from agents import Agent

import ctrlrun_openai_agents as gate
from ctrlrun import Control, InterruptApprovalProvider, Policy, SQLiteStateStore, protect
from ctrlrun_openai_agents import AgentsInterrupt, protected_tool

store = SQLiteStateStore(".ctrlrun/state.db")
control = Control(Policy.from_file("ctrlrun.yaml"), store, approvals=InterruptApprovalProvider(store, AgentsInterrupt()))


@protect("stripe.refund", effect="refund:{payment_id}", wait=True, control=control)
def issue_refund(payment_id: str, amount: int) -> str:
    return stripe.Refund.create(payment_intent=payment_id, amount=amount).status


async def refund_tool(payment_id: str, amount: int) -> str:
    """Issue a refund for a payment. Amounts are in integer minor units."""
    return issue_refund(payment_id=payment_id, amount=amount)


agent = Agent(name="refunds", tools=[protected_tool(control, "stripe.refund", refund_tool)])

result = await gate.run(agent, "refund txn_2 by 2500 euros")
if result.interruptions:
    state = result.to_state()
    for item in result.interruptions:
        print("a human decides on", item.name, item.arguments)
        state.approve(item)                     # or state.reject(item)
    result = await gate.run(agent, state)
print(result.final_output)
```

## What the agent sees

The SDK asks before invoking, because `protected_tool` answers its `needs_approval` from the
policy; the run returns with one interruption naming the tool and its arguments. After
`state.approve(item)` the resumed run invokes the tool with exactly that call's arguments, and
CTRLRun records `openai-agents:tool-approval` as the approver. A refusal by CTRLRun, a
duplicate for instance, reaches your `except` as `DuplicateEffect` through `gate.run`, not the
model as "please try again".

## The receipt

`approve/committed`. The binding across the interrupt is the SDK's, keyed by `call_id`, so the
receipt attributes the approval and cannot re-check the arguments against the hash: that is
attribution, and the adapter's README says so in that word. A rejection leaves no CTRLRun
evidence at all, because the SDK never invokes a rejected tool; record it where you call
`state.reject(item)`.

## When an AMBIGUOUS appears

The SDK's default would surface a lost reply to the model as text, and the measured behaviour
is that the model retries until the refund lands three or four times. With `effect=` declared,
the retry is refused with `AmbiguousEffect`, which `gate.run` returns as itself; resolve with
`ctrlrun resolve refund:txn_2 --committed` or `--failed`.

## Next

* [Use the OpenAI Agents SDK adapter](/guides/openai-agents-adapter): where the SDK shows through, and `ApprovalNotAsked`.
* [Three ways in](/get-started/three-ways-in) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [Cookbook](/cookbook/index.md)
- [Use the OpenAI Agents SDK adapter](/guides/openai-agents-adapter.md)
- [Three ways in](/get-started/three-ways-in.md)
- [Choosing between them](/get-started/choosing.md)
- [CTRLRun and framework human-in-the-loop](/compare/framework-hitl.md)
