# Batch Approval

> Pattern 06 · Approving actions · part of Agent Consent Patterns.
> Human page: https://agentconsent.dev/patterns/batch-approval/

When an agent proposes a run of actions, approving them one modal at a time breeds fatigue, and fatigue breeds rubber-stamping. Give the queue triage tools, but keep the highest-stakes items out of the group pass.

## Problem

An agent that works in bursts produces work in bursts: six replies to send, eight receipts to file, a thread to delete. Present these as six separate "Are you sure?" modals and the user does what anyone does with a repetitive gate, finds the rhythm and clicks through it. The fifth confirmation gets the same half-second glance as the first, which means the one that actually mattered got a half-second glance too.

The naive fix (a single "Approve all") solves the fatigue by removing the review entirely. Now the destructive item rides along with the routine ones, approved by a click that was really aimed at the fifteen emails above it. The real problem is that **triage and scrutiny pull in opposite directions**, and a good queue has to serve both: fast disposal of the obvious, a hard stop on the consequential.

## Solution

Make the queue selectable, and make selection *refuse* the items that need a person. Routine actions get a checkbox and can be swept together; high-stakes actions are flagged, excluded from select-all, and can only be resolved on their own row.

- **Batch what's alike.** Select many routine items and approve or reject them in one action, with a live count of exactly how many the action will touch.
- **Fence what's dangerous.** An item marked *review individually* is never selectable in bulk. "Select all" reaches every other item but not this one, so the group pass is safe by construction, not by the user remembering to deselect it.
- **Keep a per-row escape hatch.** Every item, flagged or not, has its own approve and reject, so a single decision never requires touching the selection.

> **Live demo** — an interactive Batch Approval demo runs on the page: https://agentconsent.dev/patterns/batch-approval/

The demo is the `BatchApproval` component from `@agentconsent/react`. Use select-all to sweep the routine items, then notice the two flagged actions sit out of the batch entirely, they'll only clear when you decide them one by one.

## Anatomy

> **Anatomy** — a labeled breakdown of the Batch Approval component's parts is shown on the interactive page: https://agentconsent.dev/patterns/batch-approval/

## When to use it

- **Bursty agent output**. A review step over a run of proposed actions (emails to send, files to move, records to update) that arrive together.
- **Homogeneous queues with a few outliers.** Batch tools earn their keep when most items are alike and safe; the flag exists for the rare item that isn't.
- **After the fact is too late.** Use this as the gate *before* execution. Once actions have run, the surface you want is an [Action Receipt](/patterns/action-receipt/), not an approval queue.

## When not to use it

- **A single action.** One proposed action is an [Action Preview](/patterns/action-preview/) or an [Irreversibility Gate](/patterns/irreversibility-gate/), not a queue, batch affordances on a queue of one are just clutter.
- **Uniformly high-stakes queues.** If every item deserves individual review, flagging all of them turns select-all into dead UI. Drop the batch controls and present a list of individual gates instead.
- **Standing, repeated approvals.** If the user approves the same *kind* of action every session, the fix is a durable rule ([Consent Memory](/patterns/consent-memory/) or an [Authority Boundary](/patterns/authority-boundary/)), not a queue they clear by hand each time.

## Real-world examples

- **Pull request review.** The canonical batch: an itemized diff, per-file navigation, and one approval that covers the set, with "request changes" as the per-item escape hatch. Every coding agent that opens PRs inherits this surface as its review step.
- **Coding-agent edit review.** Claude Code and Cursor present a run of proposed file edits as a reviewable set, accept all, or open and decide each diff individually. Shell commands stay outside the blanket accept, which is the fence-the-dangerous-item rule in shipped form.
- **Deploy approvals and grouped updates.** GitHub Actions environment protection holds a deployment until a human approves everything it ships as one gate; Dependabot's grouped updates turn a pile of routine version bumps into a single reviewable unit instead of twenty separate approvals.

*Annotated screenshots of these flows are being collected. Products are credited, annotations follow the site-wide callout conventions, and any screenshot is removed on request. See [About](/about/).*

## Accessibility

- The queue is a `role="group"` labelled by its title, so screen readers announce the scope ("Agent proposed 6 actions") on entry.
- **Select-all reflects mixed state.** The checkbox is `indeterminate` when some, but not all, selectable items are chosen, and `checked` only when the whole eligible set is, so its state never overstates the selection.
- **The selection count is a polite live region.** Selecting or clearing items announces the new count without stealing focus, so a non-visual user always knows the blast radius of the next batch action.
- **Flagged items are excluded structurally, not visually.** A must-review item renders no selection checkbox at all (not a styled-disabled control that a screen reader might still offer), and its title is plain text, not a checkbox label.
- Batch buttons expose their empty-selection state with a real `disabled` attribute plus `aria-disabled`, and every row's own approve/reject is a standard, individually labelled button.
- The flag is carried as **text** ("Review individually"), never color or a border alone.

## Anti-patterns

- **"Approve all" that means all.** A blanket approve that reaches the destructive item is the pattern's whole reason to exist. If select-all can sweep the thing you'd regret, the queue is a fatigue machine.
- **Deselect-to-be-safe.** Making the dangerous item *selected by default* and relying on the user to uncheck it inverts the safe default. Flag it out of the batch instead.
- **Hiding the count.** A batch action that won't say how many items it will touch asks the user to approve an unknown number. Always show the number, and update it live.
- **Fatigue by a thousand modals.** The opposite failure (forcing individual confirmation on genuinely routine, reversible items) trains the exact click-through reflex the flag is trying to preserve scrutiny for.
- **A queue as a dumping ground.** If everything lands here, nothing gets read. Route standing, repeatable approvals to a durable rule and keep the queue for the genuinely one-off.

## Code

```tsx
import { BatchApproval } from "@agentconsent/react";
import "@agentconsent/react/theme.css";

// Declare the queue once, requiresReview items are fenced out of bulk.
const items = [
  { id: "reply-dana" },
  { id: "archive-news" },
  { id: "delete-thread", requiresReview: true },
];

<BatchApproval.Root
  items={items}
  value={selected}
  onValueChange={setSelected}
  onApprove={(ids) => execute(ids)}
  onReject={(ids) => dismiss(ids)}
>
  <BatchApproval.Header>
    <BatchApproval.Icon>▤</BatchApproval.Icon>
    <BatchApproval.Title>Agent proposed 3 actions</BatchApproval.Title>
  </BatchApproval.Header>

  <BatchApproval.Toolbar>
    <BatchApproval.SelectAll />
    <BatchApproval.SelectionCount>
      {({ selectedCount, selectableCount }) =>
        `${selectedCount} of ${selectableCount} selected`}
    </BatchApproval.SelectionCount>
    <BatchApproval.BatchReject>Reject selected</BatchApproval.BatchReject>
    <BatchApproval.BatchApprove>Approve selected</BatchApproval.BatchApprove>
  </BatchApproval.Toolbar>

  <BatchApproval.List>
    {actions.map((action) => (
      <BatchApproval.Item
        key={action.id}
        id={action.id}
        title={action.title}
        detail={action.detail}
      >
        <BatchApproval.ItemReject>Reject</BatchApproval.ItemReject>
        <BatchApproval.ItemApprove>Approve</BatchApproval.ItemApprove>
      </BatchApproval.Item>
    ))}
  </BatchApproval.List>
</BatchApproval.Root>
```

The root's `items` prop is the single source of truth for what's selectable:
mark an item `requiresReview` and it drops out of select-all and the batch
actions automatically. Approving or rejecting ids clears them from the
selection, so a controlled queue can remove the resolved rows on the callback.
All parts are unstyled primitives with `data-acp` attributes; skip `theme.css`
and style them yourself, or override the `--acp-*` tokens to retheme.
