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

# Approval Gates

> Pause an agent run for human review, then approve, reject, or comment from the AgentUse approval dashboard

Approval gates let an agent prepare work, pause before finalizing it, and continue only after a reviewer approves, rejects, or comments.

Every run is a session. The review surface is the unified **session page** at `/sessions/<id>` in `agentuse serve`: it shows the run log, and when the session is suspended on an approval gate it also offers approve / reject / comment right there. `/approvals` remains as a filtered list of sessions currently waiting for review, and each row links to the session page. Slack is optional: it can notify reviewers and link them into the session page, but Slack is not where approval state lives.

## How It Works

1. Add `approval: true` to the agent frontmatter.
2. Run `agentuse serve`.
3. The agent works normally until it reaches the approval point.
4. AgentUse suspends the session on a pending approval gate.
5. Reviewers open the session page (`/sessions/<id>`), either from `/approvals` or from a tokenized link.
6. A reviewer approves, rejects, or comments.
7. AgentUse resumes the suspended session with the reviewer result.
8. After the session finishes, reviewers can send follow-up instructions from the same session page to continue the session with its existing context.

<Note>
  A leaf agent's approval gate works the **same whether the agent is run directly or delegated by a `type: manager`** agent. See [Approval in delegated sub-agents](#approval-in-delegated-sub-agents) below.
</Note>

## Create an Agent with Approval

Use the smallest possible config:

```md theme={"system"}
---
model: anthropic:claude-sonnet-5
approval: true
---

Write a polished launch email from the user request.
```

The markdown body stays focused on the work. You do not need to write instructions like "wait for approval before publishing."

If an approval should expire, set a timeout:

```yaml theme={"system"}
approval:
  timeout: 24h
```

By default, approvals do not expire.

## Define the Approval Boundary

Approval gates are agent-driven: the agent calls the internal `await_human` tool when it reaches the point where review is needed. AgentUse injects the approval mechanism, but your agent instructions can still define the policy boundary for your workflow.

Write the boundary in business terms. Describe what the agent may prepare without approval, and what it must not finalize until approval is granted.

For a social publishing agent:

```md theme={"system"}
---
model: anthropic:claude-sonnet-5
approval: true
channels:
  slack:
    events: [approval]
---

Draft the social post and save any working files under `.agentuse/tmp/`.

You may create drafts, previews, and local artifacts without approval.

Before scheduling or publishing the post through any external service, request approval with:
- a short summary of the post
- the final post text
- the target account or channel
- the scheduled time, if any
- any risks or assumptions
```

For a release agent:

```md theme={"system"}
---
model: anthropic:claude-sonnet-5
approval:
  timeout: 24h
---

Prepare the release notes and open a draft pull request if needed.

Do not publish the package, tag the release, merge the PR, or announce the release until the approval gate is approved.
```

Good approval boundaries usually separate reversible preparation from external side effects:

* Usually safe before approval: local temp files, drafts, previews, dry runs, summaries, draft PRs
* Usually approval-required: publishing, sending email, scheduling posts, deploying, merging, deleting, charging, or changing production/external state
* If unsure, ask for approval before the action

## Gated commands (mechanical enforcement)

The boundary above is guidance the model follows. For irreversible **bash commands** you can also enforce it mechanically, so the command cannot run before you approve it even if the model tries. List those commands under `tools.bash.gated`:

```yaml theme={"system"}
model: anthropic:claude-sonnet-5
tools:
  bash:
    commands:        # auto-run, no approval
      - "birdc read *"
      - "birdc search *"
    gated:           # run only after you approve the exact action
      - "birdc reply *"
```

Read it as: `commands` is what the agent can do; `gated` is what it can't do without you. A gated command is still allowed to run (the effective allowlist is `commands` plus `gated`), but only after a human approves the exact action. A command matching both lists is gated (gated wins), so a broad `birdc *` in `commands` cannot un-gate `birdc reply *`.

Declaring `gated` enables the approval machinery automatically, you do not also need `approval: true`. For a plain yes/no decision, the agent emits `await_human` and the gated command together. AgentUse attaches the exact command to the request, blocks it from executing, and lets the agent re-issue it only after you approve.

The guarantee is two-fold, enforced per action (not once per session):

1. A gated command never runs before approval.
2. A gated command never runs anything other than what you approved. Approval binds to the exact complete shell command shown in `changes[]`; payload text by itself grants nothing. If the agent revises the command or its payload, the old approval no longer covers it and it must re-gate.
3. Each approved entry is one-shot. It is consumed before dispatch, so an ambiguous failed external action is never retried silently. Listing the same exact command twice authorizes two executions.

Notes:

* **You rarely hand-write this.** Skills that ship posting commands, the interactive command prompt, and generated agent files add `gated` for you. `agentuse doctor <file>` also flags an allowlisted command that looks irreversible (`reply`, `post`, `delete`, `git push`, `curl -X POST`, ...) but is not gated. AgentUse never auto-gates from a keyword guess, it only suggests.
* **Plain gates auto-attach the command.** Emit `await_human` and the exact gated bash command in the same model step. The command is shown to the reviewer but remains blocked until approval; after approval the agent re-issues it once. Non-gated tools still cannot ride beside a gate.
* **Pick gates scope actions explicitly.** Give each `changes[]` action an `optionId` referencing the corresponding `options[].id`. When `content` is an executable command, also set `displayContent` to the exact human-facing post, reply, message, or body. The reviewer sees the business content first with the command de-emphasized inside the same option. Approval grants only the selected option plus any action with no `optionId`. This is the safe one-approval pattern for choosing among several irreversible commands.
* **Show the complete command.** Whether auto-attached or option-scoped, the lease covers the exact executable command (`birdc reply "text"`), not payload text alone. For browser automation, gate and show the step that carries the words (`agent-browser type "..."`), not a contentless click.
* **Unattended runs suspend only on a declared gate**, never on a tooling question. You approve outcomes, not commands.

## Start the Server

Start the AgentUse daemon:

```bash theme={"system"}
agentuse serve
```

Open the sessions dashboard (every run) or the approvals view (sessions waiting for review):

```text theme={"system"}
http://127.0.0.1:12233/sessions
http://127.0.0.1:12233/approvals
```

Both link into the per-session page at `/sessions/<id>`.

If reviewers open links from another machine, start `serve` with a reachable public URL:

```bash theme={"system"}
agentuse serve --public-url https://agentuse.mycompany.com
```

AgentUse runs one serve daemon at a time. When the daemon is serving this project, approval links use its registered port. Use `--public-url` when reviewers need a URL other than the local server address.

## Optional Slack Channel

Slack can alert reviewers when an approval is waiting. Approval state still lives in the AgentUse session page.
The Slack channel message stays concise, and supporting approval details are posted in the message thread when the agent provides them. Those thread details can include summary, draft, artifact, context, and risk.

```yaml theme={"system"}
approval: true

channels:
  slack:
    events: [approval]
```

See [Channels](/guides/channels) for Slack setup, channel ids, events, and completion/failure channel messages.

## Reviewer Flow

Make each approval understandable without opening Draft or Context or reading "Why this request". Choose one primary review surface:

* **Actions:** use `prompt` + `changes`. Put the exact caption/body in `displayContent` when `content` is a command, attach the media in `media_urls`, and name the destination account/platform and timing in the action label. Material revisions belong in the visible prompt or action label, and critical consequences belong in `risk`.
* **Standalone deliverables:** first create and save the report, proposal, or other artifact using `tools__artifact_save` when available or another suitable creation tool. Wait for success, then call `await_human` with its returned `artifact_path` (or `artifact_paths` for several files). Use `artifact_url` for an existing hosted deliverable. Never invent paths or URLs. Artifacts appear in an expanded section; supported local formats and hosted media have inline previews, and hosted documents have a visible open link.

Omit `draft`, `summary`, and `context` by default. Existing payloads remain supported.

The session page supports these review fields:

* `prompt`: the reviewer-facing decision prompt
* `changes`: the exact actions taken on approval, one `{ label, content }` entry per discrete action. Each renders as a highlighted "On approval" box with a copy button and character count, so the reviewer skims the verbatim payload first. When `content` is a command, `displayContent` carries the human-facing body; for a thread or any multi-post submission, pass `displayContent` as a list of strings, one per post, and the card renders them as numbered posts. Add `media_urls` when an action carries an external image, video, or audio asset; supported media renders inline in the same action card for review
* `reference`: the original item the action responds to (`{ label, author, title, url, excerpt }`), such as the post being commented on or the message being replied to. On a wide screen it renders in a column beside the changes, so the reviewer reads original and response together; on a narrow one it stacks above them
* `options`: two or more choices for the reviewer, each with an `id`, `label`,
  and optional `description` / `recommended`. The session page renders a
  single-choice picker before the decision controls. On approval, the selected
  id reaches the agent as `toolResult.choice`
* `summary`: optional background under "Why this request"; never the only place essential review information appears
* `draft`: legacy inline Markdown, retained for compatibility. Use actions or saved artifacts in new requests. When `changes` carries the verbatim payload, the draft renders collapsed as supporting detail
* `draft_url`: URL to a draft artifact
* `artifact_url`: URL to the primary review artifact, such as a PR, preview, document, or generated artifact
* `artifact_path` / `artifact_paths`: project-relative local files the session page can open in the artifact viewer. Images, HTML, and PDF artifacts render inline; MP4/WebM/MOV/M4V video and MP3/M4A/WAV/OGG audio use native players. Files are snapshotted at gate time so the reviewer approves immutable bytes
* `context`: background the other fields do not already carry (constraints, inputs used, process notes); agents are steered to omit it rather than repeat the reference, changes, or summary
* `risk`: known risks, unresolved questions, or reviewer attention areas
* Session logs before and after the decision

Review bodies support Markdown, including headings, emphasis, links, blockquotes,
numbered lists (`1.` or `1)`), bullet lists, tables, and fenced code. This includes
`changes[].displayContent`, individual thread posts, and `reference.excerpt`.
Preserve the exact submitted body and original quote, including any existing
Markdown. Copy buttons retain the source text; rendering does not change the
approved payload or executable command. Raw HTML is escaped.

### The original is mandatory on a response gate

When the gate approves a reply, comment, answer, or any other response to
something someone else wrote, `reference` is required and its `excerpt` must
carry the **complete verbatim original**, not a summary and not the first few
lines. The schema rejects three shapes that force the reviewer to leave the
card and open the source:

* a response gate with no `reference` at all (putting the original in `context`
  prose does not count)
* a `reference` that carries an author, title, or `url` but no `excerpt`
* an `excerpt` with an explicit elision marker such as `[truncated]` or
  `(full text at the link)`

A gate for a *fresh* post, email, or document is unaffected: it answers nothing,
so it has no original to quote.

Reviewer actions are fixed:

* `Approve`: resumes the agent with `status: "approve"`
* `Reject`: resumes the agent with `status: "reject"`
* `Comment`: resumes the agent with `status: "comment"` and the reviewer comment

`Comment` is the revise-and-re-gate path, not a failed selection. On a choice
gate, an actionable edit or direct replacement in the comment takes precedence
over the absence of a selected option. For example, `reply "very nice!"` means
to replace the proposed reply with `very nice!` and request approval again; it
is not ambiguous merely because it names no option. Only an explicit request to
cancel, abandon, or stop makes a comment terminal.

For a choice gate, AgentUse preselects the recommended option, or the first
option when none is recommended. The reviewer can change that selection before
approving. The result retains the normal approval status and adds the selected
option id:

```json theme={"system"}
{
  "status": "approve",
  "choice": "ship_tuesday"
}
```

Use stable machine-readable ids. Labels and descriptions may change without
breaking the agent logic that consumes `toolResult.choice`.

On reject, AgentUse passes the decision back to the agent instead of applying a
universal workflow state change. If the agent tracks drafts, queue items, or
tasks in a store, describe the rejected-state transition in the agent
instructions, such as "when rejected, mark the current draft item `rejected`"
or "when rejected with feedback, mark it `needs_revision`."

When Slack Socket Mode is configured, the approval details thread can also accept Approve, Reject, Comment, and normal thread replies. Without Socket Mode, reviewers open the session page link to decide.

When the agent receives a comment, it decides whether to revise, ask for approval again, or stop with a clear explanation.

After the run completes, the same session page becomes a lightweight continuation surface. Add a follow-up instruction in the **Resume session** box to resume the completed or errored session with its existing context. This matches Slack thread replies, but keeps the web session page as the source of truth for details and logs.

If an approval decision was submitted but the resumed run failed downstream, the session page may offer **Retry**. Retry reopens the latest resolved approval gate, preserves the original session token, and lets the reviewer submit the decision again. Use it only when it is safe to retry the downstream work; the UI warns about possible duplicate external side effects.

If a decision itself fails, the approval card says what failed, whether the decision was applied, and what to do next: **Reconnect** or **Connect** for a provider sign-in, **Retry** for a transient worker or provider failure, and nothing when the gate was already answered, changed, ended, or expired. Your typed comment is kept. Draft, revision, and change set pages show the same panel.

## Approval in delegated sub-agents

A leaf agent's approval gate behaves identically whether you run it directly or a `type: manager` agent delegates to it. You define `approval: true` once on the leaf, and the same human gate fires in both modes. Only the channel the request surfaces through changes.

```md title="reply-to-post.agentuse — the leaf, reused in both modes" theme={"system"}
---
model: anthropic:claude-sonnet-5
approval: true
---

Draft a reply to the given post in the brand voice, then publish it.
```

* **Run it directly** (`agentuse run reply-to-post.agentuse`) and you get the gate, as for any top-level agent.
* **Delegate it** from a manager and the *same* gate fires inside the sub-agent. The request surfaces **on the manager run's session page** (and the `/approvals` inbox, and Slack if configured), labeled with the originating leaf. Approve / reject / comment there: the decision resolves the leaf's gate, the leaf finishes, and the manager resumes automatically.

```md title="manager.agentuse — delegates to the same leaf, unchanged" theme={"system"}
---
type: manager
schedule: "0 9 * * *"
subagents:
  - path: ./reply-to-post.agentuse
---

For each post worth replying to, delegate to reply-to-post.
```

This works for an interactive manager and for a scheduled/headless one: the run suspends durably and resumes when the decision arrives.

<Note>
  **Approve at the manager root.** The delegated leaf's own session page is view-only and links up to the manager run, where the decision is made. This keeps one canonical approval surface per run.
</Note>

**v1 behavior and limits:**

* One approval at a time per manager run. To gate several items, have a single leaf loop and call the gate once per item (it re-suspends and re-surfaces at the root each time), rather than fanning out to many sub-agents that suspend simultaneously, concurrent multi-gate fan-out is not yet supported.
* The human always decides outward/irreversible actions (human relay). Automatic, classifier-based approval of low-risk calls is a separate, later option.
* Resolve a delegated gate from the manager's approval page, `/approvals` inbox, or Slack, not `agentuse sessions resume`, which handles only single-session gates.

## Configuration Fields

Approval config is intentionally small:

```yaml theme={"system"}
approval: true
```

or:

```yaml theme={"system"}
approval:
  timeout: 24h
```

* `approval`: `true` or an object to enable the approval gate
* `approval.timeout`: optional suspension timeout, such as `30m`, `24h`, or `7d`. A bare number means seconds.
* `tools.bash.gated`: bash command patterns that run only after human approval, enforced by a per-action lease. Declaring it implies the approval gate. See [Gated commands](#gated-commands-mechanical-enforcement).

Channel config is separate:

```yaml theme={"system"}
channels:
  slack:
    events: [approval]
    channel_id: C0123456789
```

* `channels`: optional external collaboration channels, such as `channels: [slack]`
* `channels.slack.events`: events for Slack. Approval gates use `approval`; terminal run events are documented in [Channels](/guides/channels).
* `channels.slack.channel_id`: Slack channel id. If omitted, AgentUse uses `SLACK_APPROVAL_CHANNEL`.
* `channels.slack.enabled`: optional switch for temporarily disabling Slack

## Auth and the session token

Authentication follows the deployment invariant:

* **Local bind** (`127.0.0.1` / `localhost` / `::1`): no API key, no token. View and approve are fully open. A keyless daemon only answers requests addressed to `localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, or the host of its [public URL](/reference/configuration-files); anything else gets `403 HOST_NOT_ALLOWED`.
* **Exposed host**: `serve` refuses to start without `AGENTUSE_API_KEY` (use `--no-auth` to bypass, dangerous). The API key (`Authorization: Bearer`) authorizes everything.

Because a `Bearer` header is not clickable from a Slack message or an email, each session also has a **session token**: a stateless, per-session value that stands in for the API key on that one session's page.

```text theme={"system"}
sessionToken = base64url( HMAC-SHA256(key = AGENTUSE_API_KEY, msg = sessionId) )
```

One token grants both **view** and **approve** for its session, and the same value works for every gate in that session. It is unguessable without the API key and is scoped to a single session (a token for session A never authorizes session B). On localhost there is no API key, so there is no token and links omit it. `await_human` mints this token when it builds the reviewer link, so Slack/email links are clickable without an API-key login.

The token covers viewing and deciding only. It cannot continue a run with a new prompt, add or discard learnings, reach the parent session, or open project files other than that session's gate artifacts, snapshots, and saved deliverables. Those need the API key, or a local keyless daemon.

<Note>
  While a run is suspended, anyone with its session link can approve it. A completed run has no pending gate, so a shared completed-log link cannot approve anything. The view + approve conflation only matters during the approval window.
</Note>

## Experimental session / approval API

The API is experimental and mirrors the web session page contract. Use it to build custom review UIs or integrations.

JSON endpoints live under the `/api/*` prefix and are always API-key gated on an exposed host. The HTML session page and its action subroutes carry their own capability auth (session token / API key / local), so a tokenized link works without a `Bearer` header.

List sessions as JSON (filter by `agent`, `trigger`, or time window with `days`):

```bash theme={"system"}
curl "http://127.0.0.1:12233/api/sessions?trigger=scheduled&days=7" \
  -H "Authorization: Bearer $AGENTUSE_API_KEY"
```

Fetch one session (summary + run-log entries):

```bash theme={"system"}
curl "http://127.0.0.1:12233/api/sessions/session_abc123" \
  -H "Authorization: Bearer $AGENTUSE_API_KEY"
```

Poll a session's live status (session token works in place of a header):

```bash theme={"system"}
curl "http://127.0.0.1:12233/sessions/session_abc123/status?token=$sessionToken"
```

Submit a decision. Authorize with the session token (`?token=`) or a `Bearer`
header; the current pending gate is resolved server-side, so you do not pass a
gate token:

```bash theme={"system"}
curl -X POST "http://127.0.0.1:12233/sessions/session_abc123/decision?token=$sessionToken" \
  -H "Content-Type: application/json" \
  -d '{ "status": "approve", "comment": "Looks good" }'
```

When a decision fails, the open-gate payload carries `actionFailure: { kind, provider?, retryable }` (plus `action`, `code`, and `message`), which the approval card uses to pick Reconnect, Retry, or nothing.

The decision endpoint returns immediately while the suspended session resumes in the background:

```json theme={"system"}
{
  "sessionId": "session_abc123",
  "status": "resuming"
}
```

Continue a completed or errored session (operator only; a session token is refused with `403 OPERATOR_REQUIRED`):

```bash theme={"system"}
curl -X POST "http://127.0.0.1:12233/sessions/session_abc123/continue" \
  -H "Authorization: Bearer $AGENTUSE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "Please revise the CTA and run the smoke test again" }'
```

The continuation endpoint also returns immediately while AgentUse starts the follow-up run:

```json theme={"system"}
{
  "sessionId": "session_abc123",
  "status": "continuing"
}
```

Reopen the latest resolved approval gate on an ended session:

```bash theme={"system"}
curl -X POST "http://127.0.0.1:12233/sessions/session_abc123/reopen?token=$sessionToken" \
  -H "Content-Type: application/json" \
  -d '{}'
```

The reopen endpoint returns the session to `suspended` so the normal decision flow can run again:

```json theme={"system"}
{
  "success": true,
  "sessionId": "session_abc123",
  "status": "suspended"
}
```

<Note>
  **Legacy approval routes.** `GET /approvals/:id` now redirects to `/sessions/:id`, and the older `/approvals` JSON list plus the `/api/approvals/:id/{status,decision,continue}` action endpoints remain available for backward compatibility. Those legacy action endpoints still take a gate `resumeToken` in the body. In-flight Slack links from before the upgrade keep working: the session page also accepts a valid gate `resumeToken` as a credential until those approvals expire. The legacy `/api/approvals/:id/continue` endpoint is operator-only as well. Prefer the `/sessions` routes and the session token for new integrations.
</Note>

## Local Testing

For development or manual testing, you can resume a suspended session from the CLI:

```bash theme={"system"}
agentuse sessions resume session_abc123 --approve
```

You can also reject or comment without writing JSON:

```bash theme={"system"}
agentuse sessions resume session_abc123 --reject "Not ready"
agentuse sessions resume session_abc123 --comment "Shorten this and add pricing"
```

Prefer the `/approvals` UI for normal review, Slack for reviewer alerts, the Approval API for custom review surfaces, and `agentuse sessions resume` for local testing.

### Testing gate flows unattended

Resuming by hand is fine for one gate. To check that each branch behaves, let `agentuse test` play the reviewer deterministically:

```bash theme={"system"}
agentuse test my-agent.agentuse --approval approve
agentuse test my-agent.agentuse --approval reject
agentuse test my-agent.agentuse --approval comment:"tighten the subject line"
```

The run finishes with no human involved, and an approval grants the gated-command lease exactly like a real reviewer, so `tools.bash.gated` flows complete end to end without executing anything irreversible. See [Testing Agents](/guides/testing-agents).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.