> For the complete documentation index, see [llms.txt](https://docs.neuron-ai.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.neuron-ai.dev/neuron-v4/agent/tool-approval.md).

# Tool Approval

Neuron provides you with full support for the human in the loop patterns including tool approval. The framework intercepts the tool call and pause, waiting for the user's final decision. To activate tool approval you need to equip your Agent with three components:

* [ToolApproval](/neuron-v4/agent/tools.md#tool-approval) middleware
* A durable [ChatHistory](/neuron-v4/agent/chat-history-and-memory.md)
* The [Persistence](/neuron-v4/workflow/persistence.md) component

```php
use NeuronAI\Agent\Middleware\ToolApproval;
use NeuronAI\Chat\History\FileChatHistory;
use NeuronAI\Workflow\Persistence\FilePersistence;

class MyAgent extends Agent
{
    ...
    
    /**
     * Register tools
     */
    protected function tools(): array
    {
        return [
            BuyTicketTool::make(),
        ];
    }

    /**
     * Attach ToolApproval middleware
     */
    protected function middleware(): array
    {
        return [
            ToolNode::class => [
                new ToolApproval()
            ],
        ];
    }
    
    /**
     * Durable chat history
     */
    protected function chatHisotry(): ChatHistoryInterface
    {
        return new FileChatHistory(
            directory: '/path-to-filesystem',
            key: 'conversation ID',
        );
    }
    
    /**
     * Workflow persistence
     */
    protected function persistence(): PersistenceInterface
    {
        return new FilePersistence(
            directory: '/path-to-filesystem'
        );
    }
}
```

The framework will manage the entire approval flow automatically. You only have to take care of the User Interface. But before dive deeper into this process let's see how you can define approval policies for your tools.

You have two ways:

**1)** Using the `requiresApproval()` method on the Tool class to determine whether approval is needed based on the tool call's arguments:

```php
class MyTool extends Tool
{
    ...,
    
    public function requiresApproval(array $inputs): bool
    {
        // The tool requires approval if the amount is greather than 100
        return $inputs['amount'] > 100;
    }
}
```

**2)** Define the policy directly in the [ToolApproval](#tool-properties) middleware via a callback:

```php
new ToolApproval(
    tools: [
        BuyTicketTool::class => function (ToolInterface $tool): bool {
            return $tool->getInput('amount') > 100;
        }
    ]
)
```

{% hint style="warning" %}
Custom approval policies on the middleware have precedence over the ones defined in the tool's `requiresApproval()` method. This allows you to override approval policies for external provided tools.
{% endhint %}

### Managing The Approval Flow

**Chat history is the system of record for tool approval.** Everything your UI needs: which tools are awaiting a decision, *why* each one is asking, and what was already decided, lives on the **last message of the thread**.&#x20;

While an approval is pending, the framework guarantees the last `ToolCallMessage` is the tail of the thread. So your UI rendering logic only ever looks at this last message.

#### Detecting Approval on UI

When you load a thread, check the last message:

1. `type` is `"tool_call"`
2. at least one entry in `tools[]` has `"approval": "pending"`

If both hold, the conversation is suspended: render the approval UI and **lock the input box**, no new user messages can be sent until all decisions have been taken.

```javascript
const last = thread.messages.at(-1);
const pendingTools = last?.type === 'tool_call'
    ? last.tools.filter(t => t.approval === 'pending')
    : [];
const isSuspended = pendingTools.length > 0;
```

#### The JSON you'll deal with

This is a real serialized `ToolCallMessage` awaiting approval: two gated tools, one still pending, one already rejected in an earlier partial submission:

```json
{
    "__id": "msg_7488585683808067584",
    "resume_token": "workflow_6650a1b2c3d4e",
    "role": "assistant",
    "content": [],
    "type": "tool_call",
    "tools": [
        {
            "callId": "toolu_01A2B3C4D5E6F7",
            "name": "delete_file",
            "description": "Delete a file from the filesystem",
            "parameters": [],
            "inputs": { "path": "C:/old_logs.txt" },
            "result": null,
            "approval": "pending",
            "approvalReason": "Deleting a file is irreversible",
            "rejectReason": null
        },
        {
            "callId": "toolu_08G9H0I1J2K3L4",
            "name": "send_email",
            "description": "Send an email to a recipient",
            "parameters": [],
            "inputs": { "to": "team@example.com", "subject": "Logs cleanup" },
            "result": null,
            "approval": "rejected",
            "approvalReason": "Outbound email reaches people outside this workspace",
            "rejectReason": "Do not email the whole team"
        }
    ]
}
```

&#x20;`approvalReason` is the *tool talking to the human* ("here's why I'm asking"), `rejectReason` is the *human talking back to the model* ("here's why I said no").

#### Rendering guidance

* Render one card per entry in `tools[]` that **has** an `approval` field: tool name, its `inputs`, the `approvalReason` if present, and Approve / Deny actions. Deny should offer an optional free-text reason — the model receives it verbatim, so a good reason produces a better model response.
* Tools already `"approved"`/`"rejected"` are shown as decided — but remain **revisable until the full set is decided** (last-write-wins). Once the last decision lands, the workflow proceeds immediately and the states become historical.
* Approvals are bare — there's no "approve with comment".

### One endpoint for the whole conversation

A normal turn and an approval resume are the same operation from the endpoint's point of view: build the agent from the thread, feed it what the client sent, and return the thread's new state. The framework sorts out the rest — with a `payload`, the resume token is adopted from the chat history tail; with a message, a fresh turn starts.

Your endpoint needs only the `threadId`:

```php
class ChatController
{
    public function chat(ChatRequest $request, string $threadId)
    {
        $agent = MyAgent::make($threadId);
    
        $handler = isset($body['decisions'])
            ? $agent->chat(payload: $body['decisions'])
            : $agent->chat(new UserMessage($body['message']));

        try {
            $handler->run();
        } catch (ChatHistoryException $e) {
            return ['status' => 'conflict', 'error' => $e->getMessage()];
        }
        
        return [
            'status'  => $handler->interrupted() ? 'awaiting_approval' : 'completed',
            'message' => $handler->getMessage()->jsonSerialize(),
        ];
    }
}
```

### Submitting decisions

Thread suspended with a pending tools. Here is the body you need to send to resolve the waiting approval:

```
POST /threads/thread_42/chat
    { "decisions": { "toolu_01A2B3C4D5E6F7": "approve" } }
```

Decisions travel as a plain map keyed by the tool's `callId`. Each value is one of exactly three forms:

| You Send                   | Meaning                      | What the model sees                                                                            |
| -------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
| "approve"                  | Run the tool                 | The tool's real output, nothing else. Approvals are bare — there is no "approve with comment". |
| "reject"                   | Skip the tool                | The rejection template with *"No specific instruction provided."*                              |
| \["reject", "your reason"] | Skip the tool, with feedback | The rejection template with your reason verbatim.                                              |
