> ## Documentation Index
> Fetch the complete documentation index at: https://sidenet.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Human-in-the-loop approvals

> Pause an agent before it writes, charges or sends — and show the user what it wants to do before it does it.

Some tool calls should not happen on a model's say-so. Set `require_approval`
on a tool and every call to it pauses the run, shows the user what the agent
is about to do, and waits for a decision. Approve, and the call runs exactly
as proposed. Decline, and the agent is told so and carries on.

## When to require approval

Anything that **writes, charges, sends, or is hard to undo**: creating a
booking, issuing a refund, sending an email, deleting a record. Not reads —
an approval prompt on every lookup trains users to click through without
looking, which defeats the one on the refund.

## Turning it on

`require_approval` is a flag on the tool's entry in an **agent's** tool list,
not on the tool itself. The same tool can require approval in one agent and
not in another.

```bash theme={null}
curl -X PATCH https://api.sidenet.ai/v1/agents/AGENT_ID/draft \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentTools": [
      { "tool_id": "TOOL_ID", "provider_id": "PROVIDER_ID", "require_approval": true }
    ]
  }'
```

In the Studio it is the **Require approval before executing this tool** badge
on the tool card, and the checkbox of the same name in the agent's tool list.
The flag saves immediately.

Like every other part of an agent's configuration, it lives on the draft:
published versions are frozen, so changing it means publishing and activating
a new version. See [Versions, environments and deploying](/docs/versions-and-environments).

<Note>
  Approvals are honoured for API tools, MCP tools and connected-app actions
  held by the agent. Tools that arrive through a per-user MCP connection built
  at request time are not yet gated.
</Note>

## What the user sees

When the agent decides to call a gated tool, the platform pauses it *before*
the call runs and asks the model for one thing first: a short sentence, in
the user's language, saying what the call will do with its decisive details —
the recipient and subject of an email, what a deletion removes. That sentence
is the headline of the approval card, next to **Approve** and **Decline**. The
proposed arguments are shown beneath it, so the user is deciding on the real
call, not a paraphrase.

The agent is told to treat the pause as the consent step. It does not ask
permission in chat first and then call the tool: it calls the tool, and the
card collects the decision. You do not need to write any of this into your own
prompt — the platform adds the guidance whenever an agent holds a gated tool.

## Deciding

**Approve** resumes the suspended run against the exact agent version that
paused, and the tool runs with the arguments the user saw. **Decline** skips
the call; the model receives "Tool call was not approved by the user", and is
instructed to treat that as a decision rather than an error — acknowledge it
and continue without repeating the call unless asked.

In the conversation history a declined call is recorded as denied, with the
reason the model gave, so a replay of the thread shows what was proposed and
that it did not happen.

## Durability

The paused run is persisted as a snapshot keyed by its run id, so it outlives
the HTTP request that started it. A client that drops mid-pause can reconnect
to the stream with `GET /v1/chat/stream` inside the disconnect grace window
and replay the approval chunk. Beyond that window an unanswered run is
abandoned; the thread is intact and the user can simply ask again.

## From the API

A gated call arrives on the chat stream as a `data-tool-call-approval` chunk
instead of a tool result. It carries everything needed to answer it:

```json theme={null}
{
  "type": "data-tool-call-approval",
  "data": {
    "runId": "run_6d20a95c",
    "toolCallId": "call_a17f3b",
    "toolName": "issue_refund",
    "reason": "Refund €120 to Maya Chen for order #4821",
    "args": { "order_id": "4821", "amount": 120, "currency": "EUR" },
    "agentVersionId": "…",
    "threadId": "…"
  }
}
```

The stream ends there — the pause finishes the turn, and no follow-up
suggestions are generated for it. To answer, `POST /v1/chat` again with the
same `threadId`, the chunk's `agentVersionId` (which skips routing so the
continuation reaches the agent that paused), and an `approval` object. On a
continuation `messages` may be empty:

```bash theme={null}
curl -X POST https://api.sidenet.ai/v1/chat \
  -H "Authorization: Bearer SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [],
    "threadId": "THREAD_ID",
    "agentVersionId": "AGENT_VERSION_ID",
    "approval": { "runId": "run_6d20a95c", "toolCallId": "call_a17f3b", "approved": true }
  }'
```

| Field        | Notes                                                                                |
| ------------ | ------------------------------------------------------------------------------------ |
| `runId`      | Required. From the chunk.                                                            |
| `toolCallId` | Optional. Disambiguates when several calls are pending; defaults to the most recent. |
| `approved`   | `true` runs the tool, `false` declines it.                                           |

The same `approval` field works on `POST /v1/agents/{id}/run` and
`POST /v1/copilots/{id}/run`. Those request/response endpoints report the
pause in their collected output — the approval chunk is present and there is
no `finishReason` — and are continued the same way. The SDK handles all of
this for you; the shapes above matter when you drive the API yourself.

## Design guidance

* **Argument names are user-facing.** They appear on the card, so
  `guest_email` reads better than `p_email_v2`. If a parameter is an internal
  id the user cannot judge, make sure the reason sentence names the thing it
  refers to.
* **Gate the write, not the read that precedes it.** Let the agent look up
  the order freely and pause only on the refund.
* **Don't double-ask.** If your prompt tells the agent to "always confirm
  before sending", it will confirm in chat and then pause again on the card.
  The card is the confirmation.
* **Pinned configuration still applies.** Values fixed on the agent's copy of
  the tool are merged in when the approved call runs, so the user approves
  the arguments the model chose and your pins fill in the rest.
