Skip to Content
DocsProductionApproval handlers

Approval handlers for Decision.ask_

StandardPermissions(mode=Mode.DEFAULT) returns Decision.ask_(...) when a tool is marked destructive=True. Without an approval handler, ask falls back to deny. The agent never silently bypasses the gate. Wire a handler to surface the decision to a human / Slack / ticket queue:

from loomflow import Agent from loomflow.core.types import ToolCall async def approve_via_slack(call: ToolCall, user_id: str | None) -> bool: """Return True to allow the tool call, False to deny.""" msg_id = await slack.post( channel="#approvals", text=f"User {user_id} wants to run {call.tool}({call.args!r}). React 👍 to approve.", ) reactions = await slack.wait_for_reaction(msg_id, timeout=300) return "👍" in reactions agent = Agent( "...", permissions=StandardPermissions(mode=Mode.DEFAULT), approval_handler=approve_via_slack, tools=[delete_user_data], # @tool(destructive=True) )

Failure-mode contract

A buggy approval flow must NEVER silently green-light a gated tool. That’s the whole point of routing destructive calls through the gate in the first place.

  • Handler returns False → tool result is denied with reason="approval declined"; the run continues, the model sees the denial in the next turn.
  • No handler wired (approval_handler=None)denied with reason="approval required; no approver". Same UX as before M10.4 . Single-tenant code that didn’t want approvals keeps working.
  • Handler raises → treated as deny + warning logged.
Last updated on