# How to Implement Custom Tool Policies with Approval Workflows in AISuite

> Implement custom tool policies with approval workflows in AISuite using RequireApprovalPolicy and callbacks. Integrate external logic for allow/deny decisions and enhance control over tool execution.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: how-to-guide
- Published: 2026-08-03

---

**Use AISuite's `RequireApprovalPolicy` class with a custom callback function to create approval workflows that intercept tool execution and defer allow/deny decisions to external logic—whether human reviewers, Slack bots, or REST APIs.**

AISuite provides a flexible **tool-policy system** that governs whether an AI agent may invoke a Python callable in a given context. This system centers on three abstractions defined in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) and [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py): policy classes that evaluate authorization, context objects that carry runtime state, and decision objects that standardize outcomes. For developers who need **approval workflows**—where a human or external service must sign off before sensitive operations run—the `RequireApprovalPolicy` class offers a plug-and-play mechanism.

## Core Components of the Policy System

Understanding the building blocks helps you wire custom approval logic correctly.

### ToolMetadata and the `@tool` Decorator

Every callable that AISuite treats as a tool is decorated with `@tool`, defined in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py). This decorator attaches `ToolMetadata` containing the tool's name, description, and optional policy override. The policy engine uses this metadata to look up which policy applies to a given invocation.

### ToolPolicyContext and ToolPolicyDecision

The `ToolPolicyContext` dataclass (from [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py)) carries runtime information:

- `tool_name`: The identifier of the tool being invoked
- `requester`: The user or principal who triggered the agent
- `args`: The arguments passed to the tool
- `kwargs`: Keyword arguments passed to the tool

Policies return a `ToolPolicyDecision` with:

- `allowed`: Boolean indicating authorization
- `reason`: Optional string explaining the decision (useful for logging and debugging)

### Policy Evaluation in the Agent Runner

[`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) implements the execution flow. Before any tool runs, the agent runner retrieves the policy (from the agent's `policy` attribute or the tool's metadata) and calls `policy.evaluate(context)`. If `allowed=False`, the tool invocation aborts immediately with the provided reason.

## Built-In Policies vs. Custom Approval Workflows

AISuite ships with four ready-made policies in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py):

| Policy | Behavior | Use Case |
|--------|----------|----------|
| `AllowAllToolPolicy` | Allows every tool unconditionally | Development, trusted environments |
| `DenyAllToolPolicy` | Blocks every tool unconditionally | Maintenance mode, incident response |
| `AllowToolsPolicy` | Allows only tools in an explicit allow-list | Scoped agents with known toolsets |
| `RequireApprovalPolicy` | Defers to a user-provided callback | **Approval workflows**, human-in-the-loop, external authorization services |

The `RequireApprovalPolicy` is the extension point for custom approval workflows. Its constructor accepts a `callback` parameter—a callable that receives a `ToolPolicyContext` and returns either a boolean or a full `ToolPolicyDecision`.

## Implementing a Human-in-the-Loop Approval Workflow

The simplest approval workflow prompts a human reviewer via the console. Replace this with your own UI, Slack integration, or ticketing system.

```python

# my_custom_policy.py

from aisuite.agents.policies import RequireApprovalPolicy, ToolPolicyDecision
from aisuite.agents.types import ToolPolicyContext

def human_approval_callback(context: ToolPolicyContext) -> ToolPolicyDecision:
    """
    Console-based approval prompt. Replace with Slack bot, email, or
    external approval service in production.
    """
    tool = context.tool_name
    user = context.requester
    
    print(f"\n🔒 APPROVAL REQUIRED")
    print(f"User: {user}")
    print(f"Tool: {tool}")
    print(f"Arguments: {context.args}")
    print("Approve? (y/n): ", end="")
    
    answer = input().strip().lower()
    approved = answer == "y"
    
    return ToolPolicyDecision(
        allowed=approved,
        reason=f"Human reviewer {'approved' if approved else 'denied'} execution of {tool}"
    )

# Export for agent configuration

approval_policy = RequireApprovalPolicy(callback=human_approval_callback)

```

Attach this policy to an agent by setting the `policy` class attribute:

```python

# agent_definition.py

from aisuite.agents.runner import AgentRunner
from aisuite.agents.policies import tool
from my_custom_policy import approval_policy

@tool
def delete_user_account(user_id: str) -> str:
    """Permanently delete a user account. Irreversible."""
    # Implementation omitted

    return f"Deleted account {user_id}"

@tool
def search_documentation(query: str) -> str:
    """Search internal documentation. Read-only, safe."""
    return f"Results for: {query}"

class SecureAdminAgent(AgentRunner):
    """
    All tools require human approval before execution.
    The policy is evaluated in aisuite/agents/runner.py before each call.
    """
    policy = approval_policy

```

When `SecureAdminAgent` attempts to invoke `delete_user_account`, execution pauses at `human_approval_callback` until the reviewer responds. The `search_documentation` tool is equally protected—you can refine this with composite policies if needed.

## Integrating External Approval Services

For production deployments, replace the console prompt with an HTTP call to an external authorization service. The callback pattern remains identical.

```python

# external_policy.py

import requests
from aisuite.agents.policies import RequireApprovalPolicy, ToolPolicyDecision
from aisuite.agents.types import ToolPolicyContext

def service_now_approval_callback(context: ToolPolicyContext) -> ToolPolicyDecision:
    """
    Submit approval request to ServiceNow or similar ITSM platform.
    Expects JSON response: {"allowed": bool, "reason": str, "ticket_id": str}
    """
    payload = {
        "requester": context.requester,
        "tool_name": context.tool_name,
        "tool_args": context.args,
        "tool_kwargs": context.kwargs,
        "timestamp": context.timestamp.isoformat() if hasattr(context, 'timestamp') else None
    }
    
    response = requests.post(
        "https://api.company.com/approvals/v1/submit",
        json=payload,
        headers={"Authorization": "Bearer " + get_service_token()},
        timeout=30
    )
    response.raise_for_status()
    
    result = response.json()
    return ToolPolicyDecision(
        allowed=result["allowed"],
        reason=f"{result['reason']} (Ticket: {result.get('ticket_id', 'N/A')})"
    )

production_policy = RequireApprovalPolicy(callback=service_now_approval_callback)

```

The external service can implement any business logic: manager approval chains, time-based restrictions, budget checks, or compliance verification. The policy system remains agnostic to these details—it simply awaits the boolean or `ToolPolicyDecision` return value.

## Advanced Pattern: Per-Tool Policy Overrides

You can attach policies to individual tools via the `@tool` decorator's metadata, overriding the agent-wide default. This enables surgical approval requirements for dangerous operations while keeping safe tools unrestricted.

```python
from aisuite.agents.policies import AllowAllToolPolicy, RequireApprovalPolicy, tool
from my_custom_policy import production_policy

# Safe tool: no approval needed

@tool
def list_active_sessions() -> list:
    """List currently active user sessions. Read-only."""
    return fetch_sessions()

# Dangerous tool: requires external approval workflow

@tool(metadata={"policy": production_policy})
def terminate_session(session_id: str) -> str:
    """Forcibly terminate an active user session."""
    # Implementation omitted

    return f"Terminated {session_id}"

class MixedSecurityAgent(AgentRunner):
    """
    Default policy allows all, but terminate_session has an override.
    Policy resolution order: tool metadata → agent policy → fallback DenyAll.
    """
    policy = AllowAllToolPolicy()

```

In [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py), the agent runner checks for a policy in `tool.metadata.policy` before falling back to `self.policy`. This precedence ensures granularity without boilerplate.

## Asynchronous and Non-Blocking Approval Patterns

The callback signature is synchronous, but you can bridge async approval workflows using blocking calls or by refactoring to use AISuite's async agent runners if available. For long-running approvals (hours or days), consider:

1. **Staging pattern**: The callback creates an approval ticket, returns `ToolPolicyDecision(allowed=False, reason="Pending ticket PROD-1234")`, and your application retries later with session resumption.
2. **Notification pattern**: The callback triggers a webhook and raises a specific exception that your orchestration layer catches for deferred execution.

These patterns extend the same callback interface without modifying AISuite internals.

## Summary

- **RequireApprovalPolicy** in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) is the extension point for custom approval workflows. It accepts any callback matching the `ToolPolicyContext → ToolPolicyDecision` signature.
- **ToolPolicyContext** provides runtime invocation details: requester identity, tool name, and arguments. Use these to route approval requests to appropriate reviewers.
- **Agent runner integration** happens automatically in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py)—policies are evaluated before every tool invocation with no additional code required.
- **Per-tool overrides** via `@tool(metadata={"policy": ...})` enable fine-grained security without agent-wide restrictions.

## Frequently Asked Questions

### Can a single callback handle multiple approval tiers based on the tool or user?

Yes. Inspect `context.tool_name` and `context.requester` within your callback to implement routing logic. For example, route `delete_*` tools to senior reviewers and `read_*` tools to automated approval, or check organizational hierarchy via an external identity service before returning the `ToolPolicyDecision`.

### What happens if the approval callback raises an exception?

The agent runner in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) propagates the exception, halting tool execution. Wrap external service calls in try/except blocks and return `ToolPolicyDecision(allowed=False, reason="Approval service unavailable")` for graceful degradation, or re-raise to fail fast depending on your reliability requirements.

### Can approval policies be composed or chained?

AISuite does not provide a built-in composition operator, but you can implement composite logic in a single callback or nest policies manually. Define a callback that iterates through sub-policies (each itself a `RequireApprovalPolicy` or custom class) and aggregates decisions with AND/OR logic before returning the final `ToolPolicyDecision`.