Approvals
Before a tool call runs, the turn can ask whether it may: that question is an approval.
- The Agent's permission policy decides which calls run, which ask, and which are refused.
- An
on_approval=function in your program answers some approvals, such as every read-only call. - On the others,
run()returnsagents.Blockeduntil a person answers them by id.
| Decision | Who answers |
|---|---|
allow | Nobody: the call runs. |
ask | Your on_approval= function, or a person, by id with turn.answer(). |
deny | Nobody: the call does not run, and the model reads the tool error Tool call denied by permission policy. |
The examples use the payment reviewer of What an agent is.
Which calls ask: the permission policy
A PermissionPolicy on the Agent decides, per tool and per argument, whether a call runs, asks or is refused. Here, the reviewer reads an alert without asking, and asks before it opens a case:
import asyncio
from mistralai import agents
from reviewer import get_alert, open_case, payment_reviewer
rules = agents.PermissionPolicy(
default="deny",
rules=[
agents.PermissionRule(tools=["client.get_alert"], decision="allow"),
agents.PermissionRule(tools=["client.open_case"], decision="ask"), # opening a case asks
],
)
reviewer = agents.Agent(
model="mistral-large-4",
name="payment-reviewer",
instructions=payment_reviewer.instructions,
tools=[get_alert, open_case],
permission_policy=rules,
)
async def policy() -> None:
state = await reviewer.run("Review alert 4711.")
print(type(state).__name__, [callback.title for callback in state.callbacks])
await reviewer.session(state.session_id).cancel()
asyncio.run(policy())Reading the alert ran. Opening a case asked, and nothing answered it, so the run returned Blocked.
A rule's tools patterns match the call's tool ID, such as client.open_case, not the name the model sees: your tools' IDs start with their namespace, client by default. The first rule that matches decides, and a call that matches no rule gets the policy's default. PermissionRule lists the other fields a rule can match on.
permission_modeOne decision for every call: permission_mode
Without rules, permission_mode= on the Agent sets one decision for every call:
permission_mode | Each tool call |
|---|---|
"auto", the default | follows the permission_policy, or runs when the Agent has none |
"ask" | asks for approval |
"deny" | is refused |
A permission_policy needs permission_mode="auto"; with another mode, the Agent raises. Subagents use the root Agent's policy: see What a subagent inherits.
on_approval=Answer with on_approval=
You answer approvals on a session, while a turn waits.
The examples below use the same reviewer with permission_mode="ask" instead of the policy, so every tool call asks. get_alert declares that it only reads:
asking_reviewer = agents.Agent(
model="mistral-large-4",
name="payment-reviewer",
instructions=payment_reviewer.instructions,
tools=[get_alert, open_case],
permission_mode="ask", # every tool call asks for approval
)Pass a function as on_approval= to agent.start_session(), agent.session(id) or agent.run(). It gets the approval's context and request, and returns agents.AllowApproval(), agents.DenyApproval(), or agents.KeepCallbackOpen() to leave the approval to a person:
def approve_reads(
ctx: agents.ApprovalCallbackContext, request: agents.ApprovalRequest
) -> agents.AllowApproval | agents.DenyApproval | agents.KeepCallbackOpen:
if request.tool.annotations.read_only_hint:
return agents.AllowApproval()
return agents.KeepCallbackOpen() # not mine: a person decides
async def review() -> None:
session = await asking_reviewer.start_session(on_approval=approve_reads)
state = await session.run("Review alert 4711.")
print(type(state).__name__)
if isinstance(state, agents.Blocked):
for callback in state.callbacks:
print(callback.title, callback.request.input["alert_id"])
await session.cancel()
asyncio.run(review())The function allowed get_alert. For open_case it returned KeepCallbackOpen(), so session.run() returned Blocked, with the open approval in callbacks. A person answers it by id: on DenyApproval(), the call does not run, and the model reads the tool error Tool call was rejected during approval. Then your program waits for the turn again: Step through approvals shows that loop.
The function runs in your program, and only while agent.run(), session.run() or turn.result() waits. It also answers the approvals of the session's subagents. Decide on request.tool.id, not on the name the model called. ApprovalHandler lists what it may return.
Answer by id
turn.answer(callback_id, answer) answers one open approval of the session or of its subagents, from any turn, and from any process that can open the session. In another process, open the turn by its ids: agent.session(session_id).turn(turn_id).answer(callback_id, answer), as in Open a session by its id.
Build a session app answers by id from a view, and shows what answer() returns when two views answer at once. Turn.answer lists its results and the errors it raises.
Approval timeout
An approval waits one hour. When nobody answers, the call does not run, the model reads that nobody approved it, and the turn continues.
Callbacks
An approval is one kind of callback, a question a turn asks your program and waits for. A turn also waits on client tool calls and on connector sign-ins. Callbacks shows how to answer all three, from a run and from a session app.
Common issues
run()returnsBlockedon an approval. The agent asks, throughpermission_mode="ask"or a rule that asks, and noon_approval=answered. Pass a function, or answer by id.- The
on_approval=function never runs. It runs only while a call waits, on a handle that has it. Pass it toagent.session(id, on_approval=)when you open the session again. - A rule never matches. Rules match the stable tool ID, such as
client.open_case, not the name the model sees. AllowApproval(scope="session")asks again.scope="session"also allows the session's later calls of the same tool with the same arguments: a call with other arguments asks again.
Next
Connectors and MCP: give the agent tools from external services.
See also:
- Step through a session: answer approvals in a loop, turn after turn.
- Build a session app: show approvals as buttons a person clicks.
- API reference: Approvals and sign-ins: every answer and result type.