---
title: Approvals
description: Make an agent's action wait for people to approve it, decide
  approvals in the console, the CLI or the API, and gate actions such as posting
  to Slack.
url: https://hexelstudio.com/docs/titan/approvals
---

An approval holds one agent action until enough of the right people approve it. Titan records the request before it answers the agent, so nothing runs until the approval is complete, and Titan runs the action itself once it is.

## How an approval works

1. A rule in your deployment's policy has the effect `require_approval` for the action, for example posting a message to a Slack channel.
2. The agent calls the capability. Titan does not run it: it records an approval request and answers the agent with `202` and the request's id (`apr_...`).
3. Approvers see the request in the console, the CLI or the API, and approve or deny it.
4. When enough approvers have approved, Titan checks the action again against the policy in force **now**, then runs it and records the execution. If that check fails, or the request has expired, the request is denied and nothing runs.
5. The agent reads the outcome with `GET /v1/titan/agent/approvals/{id}`. Only the requesting agent, on the same deployment and runtime principal, can read it.

A single **deny** ends the request at once.

## Who can approve

The rule names one or more **approval profiles** (`approval_profile_ids`), and each profile is defined in the same policy:

| Field | Meaning |
| --- | --- |
| `role_ids` | Approval roles whose members may approve. A role (`approval_roles`) lists people by their user id (`human_principal_ids`). |
| `quorum` | How many approvals complete the request. |
| `requester_separation_required` | The person who made the request can't approve it. |
| `maximum_age_seconds` | How long the request stays open. |

To decide, a person also needs the `titan:approval:decide` permission in your organization, and `titan:approval:read` to list requests. Someone who isn't a member of an applicable role is refused with `403 NOT_AN_ELIGIBLE_APPROVER`.

### How long a request stays open

A request expires at the earliest of:

- the profile's `maximum_age_seconds`;
- 24 hours;
- the end of the agent's current runtime token, which lasts 5 minutes.

On Titan Cloud an agent's approval therefore has to be decided within a few minutes. An expired request reads as `expired` and can't be approved.

## Decide in the console

Open **Titan → Approvals** in the [console](https://console.hexelstudio.com), find the pending request, and approve or deny it.

## Decide with the CLI

```bash
hexel titan approval list            # pending requests; --state all for every state
hexel titan approval approve apr_0123456789abcdef0123456789abcdef
hexel titan approval deny apr_0123456789abcdef0123456789abcdef
```

See [`hexel titan approval`](https://hexelstudio.com/docs/cli/commands) in the command reference.

## Decide with the API

List pending requests:

```bash
curl "https://api.hexelstudio.com/v1/titan/admin/approvals?state=pending" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Titan-Enforcement-Deployment: $TITAN_ENFORCEMENT_ID"
```

`state` is one of `pending`, `approved`, `denied`, `revoked`, `expired` or `invalidated`. Each item has the `approval_request_id`, the `capability`, `operation`, `resource_type` and `resource_id`, the requesting agent, `recorded_approvals` and `expires_at`.

Approve or deny one:

```bash
curl -X POST "https://api.hexelstudio.com/v1/titan/admin/approvals/apr_0123456789abcdef0123456789abcdef:decide" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Titan-Enforcement-Deployment: $TITAN_ENFORCEMENT_ID" \
  -H "Content-Type: application/json" \
  -d '{"decision": "approve"}'
```

`decision` is `approve` or `deny`. The response is the request's new `state`:

| `state` | Meaning |
| --- | --- |
| `pending` | Your vote is recorded; more approvals are needed. |
| `approved` | The quorum was reached and Titan ran the action. `execution_id` names the execution in the [Agent Ledger](https://hexelstudio.com/docs/titan/agent-ledger). |
| `denied` | You denied it, or the action no longer passed the policy check. |
| `expired` | The request expired before the quorum was reached. |

Deciding a request that is no longer pending returns `409 APPROVAL_NOT_PENDING`.

## Example: approve before an agent posts to Slack

On Titan Cloud, approvals are decided in the console, the CLI or the API. Titan doesn't send approval requests into Slack. What you can do is make an agent's Slack actions wait for approval.

With a Slack connection on your deployment, a policy can allow the agent to read channels and require approval for every post to one channel:

```json
{
  "approval_roles": [
    { "role_id": "release-approvers", "human_principal_ids": ["<user id>"] }
  ],
  "approval_profiles": [
    {
      "approval_profile_id": "post-approval",
      "role_ids": ["release-approvers"],
      "quorum": 1,
      "requester_separation_required": false,
      "maximum_age_seconds": 300
    }
  ],
  "rules": [
    {
      "rule_id": "approve-slack-post",
      "effect": "require_approval",
      "match": { "capability_ids": ["slack.message.post"] },
      "approval_profile_ids": ["post-approval"]
    }
  ]
}
```

This is an excerpt of a policy, not a complete one; your deployment's policy also needs its other required fields and its allow rules. When the agent posts, it gets `202` with the request id. An approver approves it in **Titan → Approvals** within the five minutes, and Titan then posts the message and records the execution.

<Note>
Titan's policy combines rules as deny over require_approval over allow. A deny rule that matches the action, such as a catch-all deny, wins over your `require_approval` rule.
</Note>
