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() returns agents.Blocked until a person answers them by id.
DecisionWho answers
allowNobody: the call runs.
askYour on_approval= function, or a person, by id with turn.answer().
denyNobody: 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

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())
Output
Blocked ['Allow tools.client.open_case?']

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.

One decision for every call: permission_mode

One decision for every call: permission_mode

Without rules, permission_mode= on the Agent sets one decision for every call:

permission_modeEach tool call
"auto", the defaultfollows 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.

Answer with 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
)
YOUR PROGRAMdrives the turnsession.run(...)on_approvalyour functionHOSTruns the turnA PERSONany process, by idAllowDenyturn.answer(id, ...)SESSIONwhat happened01tool calllist_branches02approvalallowed by your function03tool calldelete_branch04approvalopen05approvalallowed by a person06agent messagedeleted 2 branches
Step 1 of 5. Your program drives the turn with session.run(). The model calls list_branches, and the turn opens an approval.Each row is a session entry.

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())
Output
Blocked
Allow tools.client.open_case? 4711

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

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

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

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

Common issues

  • run() returns Blocked on an approval. The agent asks, through permission_mode="ask" or a rule that asks, and no on_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 to agent.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.

Connectors and MCP: give the agent tools from external services.

See also: