> Source: https://wexa.ai/docs/administration/approval-workflows

# Approvals

Wexa can pause a piece of work and wait for a person to approve it. Every approval in Wexa goes
into one Inbox and is decided there. This page says where approvals come from, what the Inbox shows,
who may decide, how long an approval stays open, and then walks one held call from end to end.

## Where approvals come from

A tool call is held for approval when one of these applies:

- **A consequential write made with an API key or an SDK session.** This is on by default for every
  project, admin API keys included. See [the write gate](#the-write-gate) below.
- **A non-administrator committing a change to the project ontology** in a project that is in
  Advanced mode.
- **A connector action the platform does not mark as safe to run without approval.** See
  [`run-connector-action`](/docs/tools/run-connector-action).
- **A call touching data the data catalog has classified as sensitive**, where catalog
  synchronisation is switched on.

The rest of the platform raises approvals into the same Inbox: flow previews, anomalies (continue,
restart or terminate the run), analytics gates, governance gates, permission requests, capability
activations, marketplace submissions, policy revisions, proactive cards, app requests made with
the `request-approval` tool, promoted agent actions, connector access and ontology proposals. Deciding the approval resumes or stops the work that was
paused.

### The write gate

In every project, a consequential tool call made with an API key or an SDK session waits for
approval. This includes table row inserts and updates, table create and rename, trigger changes and
connector actions, as well as tools that were already consequential, such as `delete-table-rows` and
`save-context`. Calls made by a person signed in to the console are not held by this gate, and
neither are calls made with any other kind of sign-in.

A project admin turns the gate off, or back on, in **Settings → Projects**, with the switch
**Hold consequential writes from API keys and SDK sessions for approval**. Only an admin of that
project can change it. A change can take up to 30 seconds to apply.

The write gate is checked before the ontology rule. While it is on, an ontology commit made with an
admin's API key is held too, in Simple and in Advanced mode.

## The Inbox

Open **Actions → Approvals** in the console (`/actions/approval-flows`). The **Inbox** tab lists
every approval for the selected project, of every type, together with the organization-level
approvals of your organization. The count on the tab is the number of open approvals.

You can filter by status, risk, requester, date range and type, and sort by priority, newest or
risk.

A member without the review capability does not see the Inbox. They see a note that their
approvals are routed to their administrators.

### What each entry shows

- A one-line summary of what was asked, its type, and an **Org** tag for an organization-level
  approval.
- Why it was raised.
- Who requested it: the user, and the agent when an agent acted for them.
- How long it has waited, and the time left before its deadline, or how long it is overdue.
- The client and the connector the request came from, when it came from one.
- Its risk: Low, Medium or High.
- Who it is with, and who delegated it, if it was delegated.

**Full context** opens the rest: the status, the deadline and expiry time, the gate, the source,
the payload and inputs, and, once decided, who decided it and the reason they gave.

For a held tool call the Inbox shows the summary line. The complete arguments that will run are in
`args` on `GET /v1/approvals/{id}`. Read them before you approve.

### Deciding

- **Approve.** A reason is optional and is recorded with the decision.
- **Reject.** A reason is required. The requester sees it.
- **Anomalies** offer **Continue** and **Restart from agent**, which approve with that choice, and
  **Terminate**, which rejects the run and needs a reason.
- **Bulk decide.** Select low-risk entries, or use **Select all low-risk**, then **Approve all** or
  **Reject**. Rejecting in bulk needs one reason for all of them. Medium and High risk approvals
  are always decided one at a time.
- **Delegate.** Hand the approval to a user or to a role such as `org_admin`, with an optional
  note. You need the same authority to delegate as to decide.

A decision happens once. If someone else decided the approval first, or it has expired, your
decision is refused with `409` and the entry shows why, for example `approval is already approved`
or `approval has expired and can no longer be decided`.

If the paused work cannot apply your decision, the decision is not recorded and the approval stays
open. The entry shows the reason, and you can try again.

### The Gates tab

The **Gates** tab lists the gates configured for the project, with the approvals each one is holding.
**Approve** and **Reject** on a gate decide that gate's oldest pending approval.

## Who may approve

The approver is always the person signed in. It is taken from your scope, never from the request
body. A decision request from the console that names an approver, with a field such as `user_id`,
`approver` or `decided_by`, is refused with `400`:

```json
400 {"error":"user_id is a server-bound argument: the approver is taken from your signed-in scope and may not be supplied"}
```

The REST decision endpoints read only `reason` (or `comment`) from the body and ignore every other
field.

Then these rules apply:

- **Your own request.** You may approve it only in a project you administer, as a project admin of
  that project or as an organization admin. Anyone else is refused with
  `you cannot approve your own request`. An API key acts for the admin who created it, so that admin
  may approve their own key's held calls in a project they administer.
- **Someone else's request.** You must be above the requester in your organization's hierarchy. If
  the gate names a role, your role must meet it. If the gate names a person, only that person may
  decide.
- **Organization-level approvals**, such as marketplace submissions and policy revisions, need an
  organization admin. Nobody may decide their own.
- **Out of scope.** An approval in another organization, or in a project your credential is not
  for, is not found. You get `404`, not a permission error.

The REST endpoints `POST /v1/approvals/{id}/approve` and `.../reject` follow the same rules.

## Deadlines

Each approval gets a deadline from its risk:

| Risk | Deadline |
|---|---|
| High | 24 hours |
| Medium | 48 hours |
| Low | 72 hours |

A held tool call is Medium risk, so its deadline is 48 hours. A gate can set its own review time
for the actions it governs, and that replaces the default.

Past the deadline the approval is **overdue**. It can still be decided. If the gate names someone to
escalate to, the approval moves to them after the time the gate sets.

At twice the deadline the approval is **expired**. It can no longer be decided, and a decision is
refused with `409`. For a held tool call the caller must make the call again.

## Statuses

| Status | Meaning |
|---|---|
| `pending` | Waiting for a decision. |
| `overdue` | Past its deadline and still waiting. It can still be decided. |
| `approved` | Approved. The paused work resumes. |
| `rejected` | Rejected. The paused work stops. |
| `expired` | Past twice its deadline. It can no longer be decided. |

While a decision is being applied, the entry shows **Deciding…** for a moment.

An approved held call also carries `consumed: true` once its resume token has been used. The SDKs
report that as the status `consumed`.

## Notifications

When an approval is created, each person who may decide it gets a console notification. When it is
decided, the requester gets a notification with the decision and the reason.

## A held call, walked end to end

This is one sequence, in order, with an API key in a project where the write gate is on.

**1. The call is held.** An `insert-table-rows` call comes back `202` with an approval id and a
resume token instead of a result:

```json
{
  "status": "pending_approval",
  "approval": {
    "approval_id": "ar_6abb9f3c0b5ae73d6cdd6d41",
    "resume_token": "hrt_ar_6abb9f3c0b5ae73d6cdd6d41.3f9c2e7a51b04d8e96a1c7f0d2b5e8a4c6f1093e7d2a5b8c4e0f6a1d9b3c7e25",
    "message": "approval required: consequential write by an SDK caller requires approval. Re-call this tool with resume_token after approval.",
    "lifecycle_id": "qlc_000412"
  }
}
```

The caller keeps the resume token. It is returned here and nowhere else, and it is the only way back
into this held call.

**2. An approver sees it.** It appears in the Inbox, and in `GET /v1/approvals`:

```json
{
  "id": "ar_6abb9f3c0b5ae73d6cdd6d41",
  "tool": "insert-table-rows",
  "what": "insert-table-rows",
  "why": "consequential write by an SDK caller requires approval",
  "requested_by": "6abb9e42c0b5ae73d6cdd6c1",
  "scope": { "org_id": "6abb9e42c0b5ae73d6cdd6bb", "project_id": "6abb9e42c0b5ae73d6cdd6cb",
             "user_id": "6abb9e42c0b5ae73d6cdd6c1", "role": "", "grants": null, "auth_kind": "" },
  "args": { "table_id": "tbl_orders", "rows": [ { "col_status": "shipped" } ] },
  "status": "pending",
  "created_at": "2026-09-29T10:15:00Z",
  "expires_at": "2026-10-01T10:15:00Z",
  "lifecycle_id": "qlc_000412"
}
```

`expires_at` is the deadline. After it the approval is overdue. It expires at twice the deadline.

**3. An approver approves it** in the Inbox, or with
`POST /v1/approvals/ar_6abb9f3c0b5ae73d6cdd6d41/approve`.

**4. The caller re-calls the same tool with the resume token**, and the call runs. The SDKs can wait
for the decision and resume for you.

**5. The token is now spent.** Presenting it again fails at `S6:approval` with `403`:

```json
{"error":"S6:approval","error_description":"resume rejected: approval not approved (not approved, already used, or not yours)"}
```

The same answer comes back for a rejected or expired approval, and for a token presented by a
different user, for a different tool or in a different project.

### The approved arguments are the ones that run

The arguments held with the approval replace whatever the resuming caller sends. An approver is
approving a fixed payload, and there is no window between approval and execution in which the
request can be changed.

## What the trail records

Every stage of a held call is recorded. The gateway's own trail, at `GET /v1/audit`, has these
events:

| Action | Actor | Kind |
|---|---|---|
| `insert-table-rows.approval.requested` | the caller | Approvals |
| `approval.approved` or `approval.rejected` | the approver, when they decide over REST | Approvals |
| `insert-table-rows.approval.redeemed` | the caller | Approvals |
| `insert-table-rows.executed` | the caller | Tool calls |

The approval's own events, requested, decided, consumed and expired, go to the platform audit trail
in the console, under **Governance → Audit & Lineage**, whether the decision was made in the Inbox or
over REST. A decision made over REST is flagged when it was self-approved. Read
[audit and compliance](/docs/administration/audit-and-compliance) for how long each trail lasts.

## Running the queue

- **Watch the deadlines.** An overdue approval can still be decided. An expired one cannot, and the
  requester has to ask again.
- **Decide from the arguments.** Reject when they are unfamiliar. Rejecting costs the requester one
  new request.
- **Decide whether you want the write gate.** If your API keys run unattended jobs, either keep the
  Inbox staffed or have a project admin turn the gate off for that project.
- **Remember the mode coupling for ontology commits.** A project switched back to Simple stops
  holding non-administrator ontology commits. The write gate still holds commits made with an API
  key or an SDK session. See
  [project mode administration](/docs/administration/project-mode-administration).

The concept treatment of why these gates exist is in
[policy and approvals](/docs/concepts/policy-and-approvals). This page is the operating manual for
the people who decide approvals.

## What is not here

`gateway.policy.denials.total` exists in the gateway's metrics code and nothing writes to it. Do not
build an alert on it. To watch the queue, use the Inbox or `GET /v1/approvals?status=pending`. See
[observability](/docs/concepts/observability).
