---
title: "Human-in-the-Loop Approvals"
description: "Hold an agent's writes and DDL until a human approves them. Approve or reject in the dashboard, set auto-approve rules for safe changes, and auto-expire stale requests."
canonical: "https://pgbeam.com/docs/approvals"
last-updated: "2026-10-29T19:01:20.000Z"
---

# Human-in-the-Loop Approvals

> Hold an agent's writes and DDL until a human approves them. Approve or reject in the dashboard, set auto-approve rules for safe changes, and auto-expire stale requests.

URL: https://pgbeam.com/docs/approvals

Approvals hold a statement before it reaches your database and wait for a human to say yes. Turn it on for a credential, and every write or DDL statement it runs is parked as an approval request instead of executing. A reviewer approves or rejects it in the dashboard, the agent's session blocks until then, and the statement runs only on approval.

This is the middle ground between read-only (no writes ever) and read-write (any write, immediately). The agent stays productive: it can draft a change and hand it to a person, rather than being told no.

Approvals apply to agent credentials and to human credentials. A junior analyst's write can be held for a senior reviewer the same way an agent's is.

## Turn it on

Add an approval rule to a policy. It names which statement kinds are held.

**CLI**

```bash
pgbeam policies create --name writer-supervised \
  --mode read_write \
  --allow public.orders \
  --approval-mode all \
  --approval-timeout-seconds 3600
```

**Dashboard**

On the policy profile, open **Approvals**, choose which statement kinds to
hold (writes, DDL, or both), and set an expiry. Pending requests show up on
the **Approvals** tab with the full context a reviewer needs (see below).

**API**

```json title="Policy with an approval rule"
{
  "name": "writer-supervised",
  "access_mode": "read_write",
  "table_allowlist": ["public.orders"],
  "approval_mode": "all",
  "approval_timeout_seconds": 3600,
  "approval_auto_max_rows": 10
}
```

`approval_mode` is one of `off`, `writes`, `ddl`, or `all`, and sets which
statement classes are held for a reviewer.

## What the agent sees

When a held statement is parked, the agent gets an LLM-readable notice on the wire and the session waits:

```text
NOTICE: statement held for approval (request apr_7f3a). Waiting for a reviewer.
```

If the request is approved, the statement runs and the agent gets its result. If it is rejected or expires, the agent gets an error explaining why, so it can move on instead of hanging.

```text
ERROR: statement rejected by reviewer (request apr_7f3a)
ERROR: approval request apr_7f3a expired after 1h
```

## What reviewers see

Each pending request on the **Approvals** tab carries the context needed to decide without opening a SQL console:

* The full SQL of the held statement and the credential that ran it, shown by its friendly name rather than an opaque `agt_…` id.
* The statement kind (for example `update`, `delete`, or a DDL kind) as a badge.
* The target tables the statement touches.
* An affected-row estimate, always labeled estimated because the data can change between the estimate and execution.
* A warning when the estimate exceeds the policy's `max_affected_rows` cap: even if the request is approved, the gateway still blocks a write that actually affects more rows than the cap.

## Approve or reject a request

Reviewers act in the dashboard, or from the API for automation:

```bash title="Approve or reject"
# Approve
curl -X POST \
  "https://api.pgbeam.com/v1/projects/{projectId}/approvals/{approvalId}/approve" \
  -H "Authorization: Bearer $PGBEAM_TOKEN"

# Reject with a reason
curl -X POST \
  "https://api.pgbeam.com/v1/projects/{projectId}/approvals/{approvalId}/reject" \
  -H "Authorization: Bearer $PGBEAM_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"reason": "touches too many rows, scope it down"}'
```

## Auto-approve rules

Holding every write on a busy credential is noise. The `approval_auto_max_rows` setting lets the safe changes through and reserves human attention for the rest. It is a single row ceiling for the whole policy: a held write that touches at most that many rows is approved automatically, and anything larger stays held.

```json title="Auto-approve writes touching at most 10 rows"
{
  "approval_mode": "writes",
  "approval_auto_max_rows": 10
}
```

PgBeam counts the rows a write would affect before it commits. A write at or under the ceiling is approved and recorded; a write over it is parked for a person. Set `approval_auto_max_rows` to `0` to auto-approve nothing. DDL and other statements with no countable row effect are never auto-approved.

An auto-approved write is recorded in the [audit log](/docs/audit-log) as an `auto_approved` event, and it is the only entry that write produces. That is a different event from `approved`, which records a person releasing a held statement that then runs and files its own outcome. Both `auto_approved` and `query` answer the `decision=allow` filter, so a report of everything the gateway let through includes the writes a policy waved past a human.

## Auto-expire

A held request that no one acts on expires after the policy's expiry window. The statement is rejected, the agent is told, and the request closes. This keeps a forgotten request from holding an agent session open forever. Every approval, rejection, and expiry is written to the [audit log](/docs/audit-log) and can fire an [`approval_requested`](/docs/audit-export) webhook.

## Related

* [Read-only enforcement](/docs/read-only): block writes entirely instead of holding them.
* [Dry-run writes](/docs/dry-run-writes): run writes for real, then roll them back.
* [Audit log](/docs/audit-log): every approval decision is recorded.
* [Audit export](/docs/audit-export): fire a webhook when a request is created.