# Should bulk API operations show their targets first?

An AI agent must never turn a vague request such as "archive inactive accounts" into an unbounded write call. Before it changes many records, it should show the exact target set, preserve the selection it showed, and require confirmation that applies only to that selection.

This sounds fussy until the first bad filter matches a live customer, a test tenant, or an account with an exception nobody put in the ticket. A human can make the same mistake, but an agent can send the request at machine speed and then keep going after the first bad result. The control that matters is not a polite warning before a request. It is a reviewable boundary between deciding who is affected and changing them.

## A count cannot describe a blast radius

A count tells you how many records an operation will affect. It does not tell you who they are. "482 subscriptions" may sound plausible while including an enterprise tenant, suspended accounts that must remain for legal reasons, or records in a region the request did not mention.

Require a target manifest. For a small operation, that manifest can list every identifier with enough context for a reviewer to recognize mistakes. For a larger operation, show the full list in an exportable review surface, plus useful groupings such as status, tenant, region, or owner. Do not make the human infer membership from a filter string alone.

A useful preview answers five concrete questions:

- Which API resource and environment will receive the write?
- Which selector produced this set?
- Which records are in the set, identified by stable IDs and human-readable fields?
- What mutation will each record receive?
- Which records were excluded by an explicit rule?

The last item catches an awkward failure: a request can be technically correct and still violate intent because a hidden exception never made it into the selector. If the operator says "all unpaid invoices except disputed ones," the preview should visibly report the disputed exclusion. Silence leaves the reviewer guessing whether the agent understood the exception or forgot it.

Do not confuse a sample with a manifest. Showing the first 20 records of a 10,000-record update proves very little. Samples help spot obvious trouble, but the stored selection must cover every record that can be changed.

## Preview and commit must bind to the same selection

A preview is only meaningful when the commit can prove it acts on the reviewed set. If an agent previews a search at 14:00 and reruns that same search at 14:05 before writing, it may affect a different population. New records may enter the filter; existing records may change state; pagination may reorder rows.

The safest API design creates a server-side selection snapshot. The preview endpoint returns a selection ID, its expiry, membership count, and a revision or digest. The commit endpoint accepts that selection ID and rejects it when the snapshot has expired or changed.

```http
POST /v1/subscriptions/selections
Content-Type: application/json

{
  "filter": {
    "status": "past_due",
    "region": "eu",
    "exclude_tags": ["disputed", "legal_hold"]
  },
  "fields": ["id", "customer_name", "status", "amount_due", "tags"]
}
```

A sound response has a shape like this:

```json
{
  "selection_id": "sel_7f2c",
  "expires_at": "2025-03-08T15:00:00Z",
  "count": 482,
  "digest": "sha256:4c76...",
  "records": [
    {"id":"sub_104","customer_name":"Northwind Parts","status":"past_due","amount_due":3100,"tags":[]},
    {"id":"sub_219","customer_name":"Orchard Studio","status":"past_due","amount_due":450,"tags":[]}
  ]
}
```

The `records` array may arrive through a cursor, but the selection ID must refer to the complete frozen membership, not only the current page. The reviewing interface can page through the result without changing the object under review.

After approval, the agent submits the saved selection rather than the original filter:

```http
POST /v1/subscriptions/bulk-actions
Content-Type: application/json
Idempotency-Key: 9b03c6f0-7dfa-4f22-b0e5-4b52ca4f1a51

{
  "selection_id": "sel_7f2c",
  "expected_digest": "sha256:4c76...",
  "action": {"type": "pause_collection", "reason": "approved credit hold review"}
}
```

The server should reject a mismatched digest with a conflict response. A successful request should return an action ID and per-record result locations, not merely `{"ok": true}`. A blanket success message hides partial completion, which is the normal failure mode of bulk work.

If the vendor API cannot create snapshots, the agent can still bind the operation by storing an ordered list of IDs, hashing a canonical representation, and sending that ID list to the write endpoint. This has limits: URL and body limits, stale records, and APIs that accept only a filter. In those cases, do not pretend the control is equivalent. Re-preview immediately before each bounded batch and stop when membership differs.

## Stable pagination decides whether review means anything

Offset pagination is a poor basis for an approval decision on changing data. Suppose the agent lists page one, sees records 1 through 100, then another process archives 20 records near the start. When the agent asks for offset 100, it can skip records that shifted upward. Inserts can also cause duplicates. The review count may remain close enough to look normal while individual membership changes.

Use a cursor issued by the service, and ask the API provider whether that cursor reads from a snapshot. A cursor that merely encodes a sort position can still drift if the sorted field changes. Sorting by a mutable `updated_at` field is especially bad when the proposed action itself updates that timestamp.

When you control the API, expose these properties explicitly:

- A snapshot identifier or an immutable high-water mark.
- A deterministic sort on an immutable identifier.
- An expiry that forces a fresh preview rather than silently returning newer data.
- A response field that says whether the caller is reading a consistent snapshot.

RFC 9110 classifies methods such as POST, PUT, PATCH, and DELETE as unsafe because they can change server state. That classification is not a workflow design, but it supports the practical rule: do not treat a list endpoint followed by an unsafe method as a single atomic operation just because the code places both calls next to each other.

For an external API that offers neither stable cursors nor selection snapshots, reduce scope until someone can review each request. It is tempting to work around the limitation with an agent-side cache and a long loop. That workaround often creates a second database with no transaction boundary and no authoritative answer when a record moves halfway through the run.

## The approval must describe the mutation, not just the records

A reviewer needs to approve both membership and effect. "Apply changes to 482 records" is empty unless the interface says whether it will delete, disable, reassign, charge, publish, or alter a field. Include the before and proposed after value for every field that will change, with a concise aggregate when all values receive the same update.

Distinguish absolute writes from conditional writes. An absolute write says `status = archived` regardless of what happened after preview. A conditional write says "archive only if status is still inactive and version is still 17." Conditional writes are usually safer because they fail closed when someone else changed the record.

Use a version, ETag, or last-known revision in each mutation where the API supports it. This is not a substitute for the target manifest. It addresses a separate problem: a reviewed record may no longer be eligible when execution starts.

A compact approval record can be represented like this:

```json
{
  "request_id": "req_91a8",
  "selection_id": "sel_7f2c",
  "selection_digest": "sha256:4c76...",
  "target_count": 482,
  "action": {
    "type": "pause_collection",
    "precondition": {"status": "past_due"}
  },
  "approved_by": "operator account identifier",
  "approved_at": "2025-03-08T14:16:02Z"
}
```

Do not let an agent reuse this approval for a different action against the same set. Pausing collection, issuing credits, and deleting records have different consequences even when the target list is identical. Bind the approval to a canonical action payload as well as the selection digest.

Time limits matter. An approval that remains valid until the agent decides to use it turns a moment of human review into a standing permission. Give approvals a short expiry appropriate to the operation, invalidate them when the selection changes, and require a new decision after the agent materially alters the action.

## Partial completion needs a journal and a stop rule

Every bulk operation eventually meets a rate limit, timeout, validation failure, or network break. The dangerous response is to retry the whole job without knowing which records already changed. That creates duplicate charges, repeated notifications, or a misleading audit trail.

Assign one operation ID and one idempotency key to the requested action. Record the outcome for each target: succeeded, failed, skipped because a precondition changed, or unknown because the service did not return a durable result. "Unknown" is not the same as failed. Treat it as an investigation state before retrying.

Set a stop rule before execution. A sensible rule might stop after a structural error, such as an authorization failure or an unexpected schema response, while allowing isolated validation failures to be collected for review. Avoid a generic "continue on error" setting. It converts an unrecognized API contract change into a long list of damaged records.

Consider a common failure. An agent previews 800 user accounts for a role change, then begins a client-side loop. The first 300 requests succeed. A deployment changes the endpoint so a missing field defaults to administrator rather than the intended viewer role. The next response looks successful. If the loop continues, the mistake spreads. If the agent records each response shape and stops when the contract differs from the approved action, the blast radius ends at the first anomalous response.

For destructive work, design compensation before execution. A compensation request needs the prior value captured per record, not a vague promise that someone can undo it. Even then, do not call a rollback harmless. A later legitimate edit can make a blind reversal wrong, and external side effects such as emails or exports may not reverse at all.

## Read access can expose as much as a bad write

Teams often apply careful confirmation to deletion and none to selection. That misses the fact that an agent may retrieve a full customer list, personal addresses, payment states, or internal notes to build its preview. The preview should expose enough data for a human to recognize records, not every field the underlying API offers.

Ask for a deliberately narrow field set. Stable ID, display name, status, ownership, and the values relevant to the proposed change usually suffice. Keep secrets, tokens, free-form notes, and unrelated personal data out of the selection response. This makes review less noisy and reduces what the agent can repeat in later messages.

The same principle applies to filters. An agent should not broaden a query because it lacks permission to inspect a field. If it cannot prove whether a record belongs in the selection, it should surface the ambiguity and wait for a human to resolve it. Guessing is not operational judgment.

Treat environment as part of the manifest. Production, staging, and a sandbox can expose identical resource names. Put the destination host or account identifier beside the target count and action summary. Engineers have approved a perfectly reasonable record list against the wrong environment because the preview made the environment look like background detail.

## Agents need authority over the call, not custody of credentials

An agent that holds a broad API token can make the bulk call before anybody sees the target set. You can add prompts and logs around that design, but the credential still gives the process an escape route. Keep the credential with an action executor that can deny or approve the request before it reaches the external API.

Sallyport takes this approach for supported HTTP and SSH actions: the agent asks through its MCP connection, while the app retains the credential and performs the action. Its per-session authorization and optional per-call key approval suit a workflow where an agent may prepare a bulk request but should not obtain reusable secret material.

That approval is not the whole bulk safety design. An approval card for an HTTP call cannot tell a reviewer whether a filter will return 10 records or 10,000 unless the agent has first produced and preserved the target manifest. Use the gateway to control authority, then make the application workflow bind preview, selection, approval, and commit.

The audit record also needs two levels of detail. One journal should show the agent run that requested the work and who approved it. Another should show each external call, including the selection digest, operation ID, endpoint, and result status. If an incident occurs, investigators need to answer both "which process asked?" and "which records changed?"

## A bulk workflow should fail closed when intent becomes ambiguous

Build the agent workflow so it cannot advance from a natural-language request straight to a mutation. The following sequence is deliberately boring because boring is easier to investigate.

1. The agent converts the request into a selector, proposed mutation, environment, and exclusions. It asks for clarification if any of those remain ambiguous.
2. It creates a stable selection and retrieves the review fields for all members. It records the selection ID, digest, query, timestamp, and page completeness.
3. It presents the manifest and the proposed effect. A human approves that exact pair or rejects it.
4. It sends a commit request with the selection reference, expected digest, action payload, idempotency key, and record versions where available.
5. It reports completed, failed, skipped, and unknown outcomes separately. It never turns a partial result into a cheerful statement that the job finished.

Do not approve a raw command such as "run the cleanup script" when the command can calculate its targets later. That practice is popular because it is fast and familiar, especially for teams that already trust their scripts. It fails because the approval covers code text, not the live data set. A minor data change between confirmation and execution can make the approved command do unapproved work.

For recurring jobs, predefine narrow selectors and a maximum count, then require review whenever the selection breaches that bound or includes an unfamiliar category. A maximum count is a guardrail, not authorization. The manifest remains the evidence of who the job actually touched.

The first implementation task is simple: make your bulk endpoint return a selection identifier and digest, then reject commit requests that do not repeat both values. Once that contract exists, agents, dashboards, and scripts all have a hard boundary they can respect.
