---
title: "Sign-offs"
description: "Your agent can tighten its own guardrails at once, but a change that loosens them waits for a workspace admin to approve it."
---

An agent may never loosen its own guardrails. When your agent, your CLI login or an MCP session asks to raise a limit, remove a budget, add an action or relax an approval policy, Anlyon does not apply the change. It records a **sign-off request** and answers `202` with a link. A workspace admin opens the link, reads what would change, and approves or rejects it. A change that tightens a guardrail applies at once, as before.

## Why sign-offs matter

A coding agent that sets Anlyon up holds a credential that can change the rules it runs under. Without sign-offs, one mistaken or manipulated step can raise the refund limit it is held to, or turn off the approval that stops it. With sign-offs:

- **The limits you set stay set.** An agent can lower a limit or add a policy on its own. It cannot raise or remove one without a person saying yes.
- **Setup still runs unattended.** Most setup tightens or adds nothing risky, so it goes through. The few changes that loosen wait for one click from an admin instead of blocking the whole run.
- **You see exactly what changes.** Each request carries a plain diff, field by field, with the reason it counts as loosening.
- **Every step is on the record.** The request, the approval or rejection, a cancellation and an expiry are each an [audit record](/trust-control/audit).

## What needs a sign-off

A change needs a sign-off when it loosens a guardrail and it comes from an API key, a CLI login, an MCP session or an end-user session token. A person signed in to the console applies changes directly.

| Change | Tightens, applies at once | Loosens, needs a sign-off |
| --- | --- | --- |
| [Impact limit](/execution/impact-limits) | Create one, lower one, rename one | Raise one, archive one |
| [Budget](/account/billing) for a key | Add a cap, lower a cap, alert earlier | Raise or remove a cap, alert later, turn the budget off |
| [Approval policy](/trust-control/approvals) | Add or tighten a policy that is at least as strict as what it takes over, delete an auto-approve policy | Add an auto-approve policy, require fewer approvals, change who decides, disable or delete a policy that gated calls, widen what auto-approve matches |
| [Policy file](/trust-control/policy-as-code) apply | Every change in the file tightens | Any one change in the file loosens |
| [Kill switch](/trust-control/environments) | Halt the environment | Resume it |
| [Action](/execution/actions) | Disable or delete one, require approval, add a read-back check | Create an enabled action, import actions, enable one, stop requiring approval, change where or how it sends |
| [Secret](/execution/secrets) | Narrow its hosts or placements, rotate its value | Widen its hosts or placements, point it at a secret store |
| Secret store | Remove one | Connect one, change one, let it reach private addresses |
| Your own OAuth app | Remove it | Set or replace it |

Anything Anlyon cannot show to be tighter counts as loosening. A new setting counts as loosening until it is classified.

## What your agent gets back

The route answers `202 Accepted` instead of applying the change. `Location` and `data.pollUrl` name the request to poll.

```json
{
  "success": true,
  "signOffRequired": true,
  "data": {
    "object": "sign_off_request",
    "id": "sor_V1StGXR8Z5jdHi6BmyT9q",
    "status": "pending",
    "operation": "impact_limit.update",
    "summary": "Raise impact limit \"Daily refunds\" from 500 to 5000 usd",
    "diff": "amount (loosens): Raises the limit.\n- 500\n+ 5000",
    "signOffUrl": "https://console.anlyon.com/sign-offs/sor_V1StGXR8Z5jdHi6BmyT9q",
    "pollUrl": "/api/v2/sign-offs/sor_V1StGXR8Z5jdHi6BmyT9q",
    "pollAfterSeconds": 5,
    "expiresAt": "2026-10-29T10:00:00.000Z"
  }
}
```

Give `signOffUrl` to a person. Then read `GET /api/v2/sign-offs/{id}` every `pollAfterSeconds` until `status` is no longer `pending`:

| Status | Meaning |
| --- | --- |
| `pending` | Waiting for a workspace admin. |
| `applied` | Approved, and the change is in effect. `result` holds what the original call would have returned. |
| `rejected` | An admin said no. Nothing changed. |
| `expired` | Nobody decided within 24 hours. Nothing changed. |
| `cancelled` | The credential that asked withdrew it with `POST /api/v2/sign-offs/{id}/cancel`. Nothing changed. |
| `failed` | Approved, but it could not be applied. `error.code` says why, such as `SIGN_OFF_STALE` when what it changes moved after it was asked for. Nothing changed. |

Asking for the same change again from the same credential returns the same pending request, so a retry never creates a second one.

## Approve or reject a request

A workspace admin decides, in one of two places:

- **The console.** **Sign-offs** lists every request with its diff. Approve applies the change; Reject leaves everything as it is.
- **The email.** Each admin is emailed a link of their own. Opening it shows the change and decides nothing, so a mail scanner cannot approve it. Approving or rejecting is a button on that page.

Requests also reach your [notification channels](/account/notifications), such as Slack, with a link to the console.

No API key, CLI login or MCP session can approve a request, whatever its scopes, including the credential that asked. You can approve your own agent's request from the console: the rule is about the channel, not the person.

## Use it from the CLI, an MCP client or the SDK

```bash title="CLI"
anlyon actions declare refund-order --ungoverned --yes --wait 10m
# Needs a human's sign-off. A workspace admin must approve this change before it applies:
#   https://console.anlyon.com/sign-offs/sor_...
# Without --wait the command exits 15. With --wait: 0 when applied, 14 when it ends any other way, 9 when the wait runs out.

anlyon sign-offs list --status pending
anlyon sign-offs get sor_V1StGXR8Z5jdHi6BmyT9q --wait 10m
anlyon sign-offs cancel sor_V1StGXR8Z5jdHi6BmyT9q
```

```ts title="TypeScript"
import { Client, SignOffRequiredError } from '@anlyonhq/sdk';

const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });

try {
  await anlyon.impactLimits.update('lim_01JABCDEF', { amount: 5000 });
} catch (error) {
  if (!(error instanceof SignOffRequiredError)) throw error;
  console.log(`Ask a workspace admin to approve: ${error.signOffUrl}`);
  const decided = await anlyon.signOffs.wait(error.signOffId);
  console.log(decided.status); // applied, rejected, expired, cancelled or failed
}
```

```python title="Python"
from anlyon import Client, SignOffRequiredError

anlyon = Client()

try:
    anlyon.impact_limits.update("lim_01JABCDEF", amount=5000)
except SignOffRequiredError as error:
    print(f"Ask a workspace admin to approve: {error.sign_off_url}")
    decided = anlyon.sign_offs.wait(error.sign_off_id)
    print(decided["status"])
```

In an MCP client, a tool call whose change is held returns a normal result that starts with "A human must approve this change before it applies. Give them this link:", followed by the diff. The agent can call `wait_for_sign_off` or `check_sign_off` with the request's id.

## Fix the errors you will meet

| Status and code | When | What to do |
| --- | --- | --- |
| `202` with `signOffRequired: true` | The change loosens a guardrail. | Give `data.signOffUrl` to a workspace admin and poll `data.pollUrl`. |
| `409 SIGN_OFF_NOT_PENDING` | Approving, rejecting or cancelling a request that already ended. | Read the request. If it expired or failed, make the change again to ask anew. |
| `400 SIGN_OFF_CANCEL_BY_REQUESTER` | A console session tried to cancel a request. | Reject it instead. |
| `403 AUTHORIZATION_ERROR` | A member who is not an admin tried to decide. | Ask a workspace admin. |

## Related

- [Approvals and policies](/trust-control/approvals): the approvals your agent's calls wait for.
- [Workspace defaults](/trust-control/workspace-defaults): the rules no default can change.
- [Audit](/trust-control/audit): the records each request leaves.
- [Authentication and API keys](/authentication): scopes and operator keys.
