---
title: "Approvals & policies"
description: "Park a production call for a human decision, route it with policy evaluated on every invocation, and record who decided what."
---

An **approval** pauses a call until a human decides, and records who decided what, when, and why. A **policy** decides, deterministically and per environment, which calls need a human at all.

This page is the concept overview. It applies to both ways of requiring a human, and they are not equivalent, so start there.

## Two shapes, and they hold different things

### Gate the action, and Anlyon holds the call

The primary shape. Mark an [action](/execution/actions) `requiresApproval`, or let a policy require it, and the invocation parks instead of executing. Anlyon is the one that would have made the request, so Anlyon is the one holding it.

```typescript
const { data } = await anlyon.actions.invoke('refund-order', {
  charge: 'ch_3P9x',
  amount: 12000,
});

if (data!.pendingApproval) {
  // 202. Nothing has been sent to Stripe.
  console.log('Waiting on a human:', data!.approvalId);
}
```

On approval, Anlyon executes the **request snapshot** frozen when the invocation was requested, using the vaulted credential. Editing the action while somebody is deciding cannot change what their yes sends.

### Gate your own function, and your process holds the call

When the call cannot move into Anlyon, wrap it. The SDK creates the approval, waits for the decision, and calls through only on approve.

```typescript
const guardedRefund = client.approvals.gate(
  { title: 'Refund a customer?', timeoutMs: 10 * 60_000 },
  refund,
);

await guardedRefund('ord_42', 129.99); // blocks until a human approves
```

A denied request raises `ApprovalDeniedError`. A timeout raises `ApprovalPendingError` and leaves the approval open.

Be precise about what this buys you. The wrapper waits for a decision before calling your function, and it depends on your code going through the wrapper. It does not move your credential into Anlyon, and Anlyon cannot attest that your function ran, or did not. You get the human decision and the record. You do not get credential isolation. See [Local approval gates](/guides/approval-gates).

Decisions arrive in the dashboard **Approvals inbox** in realtime for either shape, or through `client.approvals.approve()` / `deny()` from your own tooling.

## Two rules, always enforced server-side

- **The agent can't approve itself.** Deciding requires the separate `approvals:decide` scope and a distinct credential. The credential that requested an approval cannot decide it. No scope grants that and no policy relaxes it.
- **Every decision is recorded**: decider, timestamp, note, and the policy (and its version) that governed the routing.

Any workspace member can decide an approval in the console.

## Policies do the routine work

Policies evaluate at request time, per environment, in ascending priority. The first match wins, so routing is deterministic and explainable:

- `auto_approve` the low-risk cases (small refunds, staging) so humans only see what matters. To clear an action's own `requiresApproval` flag the policy must name that action with `matchActionName`. A catch-all `auto_approve` cannot disarm every gated action at once.
- `require_approval`, optionally with **N-of-M approvers**, for the consequential ones.
- `auto_deny` what should never happen.

**N-of-M, done safely**

A policy can require several approvers. Each decider gets one vote (enforced by a unique index), any single deny finalizes the request, and a gated action executes on the finalizing approval, once. N counts distinct credentials for API-key deciders and distinct users for dashboard and OAuth deciders. Two keys held by one person are two deciders.

Create and version policies in the dashboard under **Approval Policies**, or from deployment code with an operator credential (see [Policy as code](/trust-control/policy-as-code)). Every policy states what it applies to: one action, every action, custom requests, or every request. Nothing is inferred from its name, and a broad `auto_deny` needs an explicit confirmation.

Every governed approval records which policy version routed it and a human-readable explanation. An automatic denial is also written onto the **invocation**, as a persisted `decision` (source, reason code, safe explanation, and the policy's name and version as they were), so an invocation explains itself even after the policy is edited or deleted.

### Who decided is recorded, not inferred

Each approval carries a `decisionOrigin`: `human`, `policy`, `system` (expiry) or `assistant` (an [Anlyon Vigil](/trust-control/vigil) approval). The console inbox opens on decisions waiting for a person. **Approver history** and **Automated decisions** are separate views, so decisions a policy made are an audit trail rather than a queue.

## Loud by default

A new workspace hears about approvals with no setup. `approval_requested` alerts go to the in-app inbox. Email alerts are included during Early Beta Access, up to 500 emails per workspace per UTC month. Once connected, alerts also go to Slack, Discord and a webhook. Each alert links to the approval. Configure channels and recipients under **Settings → Notifications**.

## It's all in the trace

Pass `runId` to the gate and the approval emits `approval_wait` and `approval_decision` spans into that run's trace. The human wait sits on the same timeline as the model and tool calls. Approvals are **never metered**, on any plan.

## Related

- [Actions](/execution/actions): defining the call Anlyon holds.
- [Bound approvals](/execution/previews): what an approval of a governed action is bound to.
- [Local approval gates](/guides/approval-gates): the hands-on guide for the wrapper.
- [Environments & the kill switch](/trust-control/environments): halting blocks even approved calls.
- [EU AI Act mapping](/trust-control/compliance): how approvals relate to the Article 14 oversight obligation.
