# commandBatches (/docs/reference/api/command-batches)

> Destructive-command batches (v1.0 quorum). A batch fixes a set of (subject, kind) leaves, optionally bound to exact payloads, under one Merkle root when it is created (modules create them through `commands.createBatch`). When the deployment publishes approver keys (`pki.approvers`), the batch's commands wait until enough approvers, holding keys off the control plane, have signed its root for the trust domain. The gateway then sends each command with the approval and its leaf's proof, and the daemon verifies both. Without approver keys a batch is recorded for audit and its commands go out at once. See [Security](/docs/operations/security).



Destructive-command batches (v1.0 quorum). A batch fixes a set of (subject, kind) leaves, optionally bound to exact payloads, under one Merkle root when it is created (modules create them through `commands.createBatch`). When the deployment publishes approver keys (`pki.approvers`), the batch's commands wait until enough approvers, holding keys off the control plane, have signed its root for the trust domain. The gateway then sends each command with the approval and its leaf's proof, and the daemon verifies both. Without approver keys a batch is recorded for audit and its commands go out at once. See [Security](/docs/operations/security).

All 7 methods are served by this release.

| Method                | Kind     | Authorization                                | Phase | Status     |
| --------------------- | -------- | -------------------------------------------- | ----- | ---------- |
| [`create`](#create)   | mutation | `compute:batches:create` @ `compute-tenant`  | v1.0  | registered |
| [`get`](#get)         | read     | `compute:batches:read` @ `compute-tenant`    | v1.0  | registered |
| [`list`](#list)       | read     | `compute:batches:read` @ `compute-tenant`    | v1.0  | registered |
| [`scope`](#scope)     | read     | `compute:batches:read` @ `compute-tenant`    | v1.0  | registered |
| [`members`](#members) | read     | `compute:batches:read` @ `compute-tenant`    | v1.0  | registered |
| [`approve`](#approve) | mutation | `compute:batches:approve` @ `compute-tenant` | v1.0  | registered |
| [`cancel`](#cancel)   | mutation | `compute:batches:approve` @ `compute-tenant` | v1.0  | registered |

## create [#create]

`POST /api/compute/commandBatches/create`

Creates a batch from its (subject, kind) leaves, each optionally bound to a payload's SHA-256, a reason and a lifetime; returns it `pending`, with the approvals its size needs.

* The tree is fixed now: its root, size and every leaf's proof are stored, so what approvers review is what the daemons check. Subjects are computed as the executing agent does (see pki `batchSubject`): the node id, `bmc:<site agent>/<address>` for BMC kinds, `node:<target>` for `site.httpProxy`. A leaf without `payloadSha256` admits any payload of its kind, and `scope` flags it to approvers.
* **Idempotency.** The batch id derives from the tenant, the caller and `idempotencyKey`. The same request again (the same leaves in any order, reason and lifetime) returns the stored batch without a new event; a different request under the key is `CONFLICT`.
* The lifetime is `notAfterSeconds` (default 24 hours): after it the batch admits no commands and can no longer be approved. At most 50,000 leaves.

Publishes `command.batch.created` with the scope size and the approvals required.

* **Kind:** mutation
* **Authorization:** `compute:batches:create` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field                     | Type           | Required | Constraints |
| ------------------------- | -------------- | -------- | ----------- |
| `tenantId`                | string         | yes      |             |
| `reason`                  | string         | yes      |             |
| `scope`                   | list of object | yes      |             |
| `scope[].subject`         | string         | yes      |             |
| `scope[].kind`            | string         | yes      |             |
| `scope[].payloadSha256`   | string         |          |             |
| `scope[].payload`         | object         |          |             |
| `scope[].payload.typeUrl` | string         | yes      |             |
| `scope[].payload.value`   | string         | yes      |             |
| `notAfterSeconds`         | integer        |          |             |
| `idempotencyKey`          | string         | yes      |             |

## get [#get]

`POST /api/compute/commandBatches/get`

Returns one batch of the tenant: its reason, state, scope root and size, the approvals it needs and has, and how many of its commands are in each state.

The state is `expired` as soon as `notAfter` passes, even before the minute sweep writes it. A batch of another tenant is `NOT_FOUND`.

`createdBy` is the identity that created the batch. When an agent proposed it on that identity's behalf (MCP `propose_command_batch`), `createdByAgent` and `delegationId` say so, and the `reason` is the agent's text, not the person's.

* **Kind:** read
* **Authorization:** `compute:batches:read` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type   | Required | Constraints |
| ---------- | ------ | -------- | ----------- |
| `tenantId` | string | yes      |             |
| `batchId`  | string | yes      |             |

## list [#list]

`POST /api/compute/commandBatches/list`

Returns the tenant's batches, newest first, filtered by `state`.

`limit` is 1 to 200 (default 50); `next`, when present, is the `cursor` for the following page.

* **Kind:** read
* **Authorization:** `compute:batches:read` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type                                                 | Required | Constraints |
| ---------- | ---------------------------------------------------- | -------- | ----------- |
| `tenantId` | string                                               | yes      |             |
| `state`    | one of `pending`, `approved`, `expired`, `cancelled` |          |             |
| `limit`    | integer                                              |          |             |
| `cursor`   | string                                               |          |             |

## scope [#scope]

`POST /api/compute/commandBatches/scope`

Returns a batch's leaves in leaf order, for approvers to review before signing: each leaf's index, subject, kind and `payloadSha256`, plus the leaf's `payload` (`{ typeUrl, value }` base64) when its creator stored it. Decode it by the kind's payload type, after checking it against `payloadSha256`.

An empty `payloadSha256` marks a leaf that admits any payload of its kind, which approvers should look at closely. Pages hold 1 to 5,000 leaves (1,000 by default); `next` is the leaf index to pass as `cursor`.

* **Kind:** read
* **Authorization:** `compute:batches:read` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type    | Required | Constraints |
| ---------- | ------- | -------- | ----------- |
| `tenantId` | string  | yes      |             |
| `batchId`  | string  | yes      |             |
| `cursor`   | integer |          |             |
| `limit`    | integer |          |             |

## members [#members]

`POST /api/compute/commandBatches/members`

Returns the commands enqueued into a batch so far, in command-id order: each one's node, (subject, kind), state and reason, and `awaitingApproval` while it is held for the batch's approval.

`state` keeps one command state. Pages hold 1 to 500 commands (100 by default); `next` is the command id to pass as `cursor`. A batch of another tenant is `NOT_FOUND`.

* **Kind:** read
* **Authorization:** `compute:batches:read` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type                                                                                                          | Required | Constraints |
| ---------- | ------------------------------------------------------------------------------------------------------------- | -------- | ----------- |
| `tenantId` | string                                                                                                        | yes      |             |
| `batchId`  | string                                                                                                        | yes      |             |
| `state`    | one of `pending`, `dispatched`, `acked`, `running`, `succeeded`, `failed`, `rejected`, `expired`, `cancelled` |          |             |
| `limit`    | integer                                                                                                       |          |             |
| `cursor`   | string                                                                                                        |          |             |

## approve [#approve]

`POST /api/compute/commandBatches/approve`

Adds one approver's signature over the batch's root; once the signatures meet the threshold for its size, the batch becomes `approved` and its held commands are released at once.

* `signature` is `{ kid, sig }`: the approver key id and a base64url P-256 signature over the batch (id, root, size, end, trust domain), made off the control plane. A signature by a key outside the published set, or over anything else, is `ACCESS_DENIED`. The same approver twice counts once.
* Decided as `compute:batches:approve`, on top of the signature, never instead of it.
* `INVALID_STATE` for a batch that is not `pending`, or one that needs no approvals (no approver keys, or a size the tiers exempt). `INVALID_CONFIG` when the deployment has no trust domain (no `pki.join`).

Publishes `command.batch.approved` with the approver key ids when the batch completes.

* **Kind:** mutation
* **Authorization:** `compute:batches:approve` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field           | Type   | Required | Constraints |
| --------------- | ------ | -------- | ----------- |
| `tenantId`      | string | yes      |             |
| `batchId`       | string | yes      |             |
| `signature`     | object | yes      |             |
| `signature.kid` | string | yes      |             |
| `signature.sig` | string | yes      |             |

## cancel [#cancel]

`POST /api/compute/commandBatches/cancel`

Ends a pending batch: it becomes `cancelled` and its held commands are cancelled with it.

A batch that is not `pending` is `INVALID_STATE`. Decided as `compute:batches:approve`.

* **Kind:** mutation
* **Authorization:** `compute:batches:approve` @ `compute-tenant`
* **Module:** `quorum`
* **Phase:** v1.0 (registered)

**Input**

| Field      | Type   | Required | Constraints |
| ---------- | ------ | -------- | ----------- |
| `tenantId` | string | yes      |             |
| `batchId`  | string | yes      |             |
