Batch endpoints
Any entity with CRUD enabled gets three transactional batch
endpoints under the _batch suffix. All items in one request run in
a single database transaction: the first per-item failure rolls back
the whole transaction.
Routes
| Method | Path | Body |
|---|---|---|
POST | /{table}/_batch | {"items":[ {…}, {…}, … ]} |
PATCH | /{table}/_batch | {"items":[ {"id":"x", …}, … ]} |
DELETE | /{table}/_batch | {"ids":["x","y", …]} |
Response envelope
{ "committed": true, "results": [ { "index": 0, "data": { "id": "p1", "title": "A" } }, { "index": 1, "data": { "id": "p2", "title": "B" } } ] }
committed: true→ HTTP 200, the transaction was applied.committed: false→ HTTP 400, the transaction was rolled back.resultsis always in input order; every input index appears.- Every batch response carries
Cache-Control: no-store, as the single-row writes do, and audit rows a batch writes carry the request's client IP and user agent. - The first per-item failure populates
erroron that index, andreasonwhen the refusal has a code (a hook's own wording has none): the same message and code the single-row write answers for that item. A validation, state, transition, unique or foreign-key refusal says what was wrong, withfieldsandfield_reasonswhere the single write names fields. Any other error readsinternal server error, and the cause stays in the server log. Later indices are marked"skipped": true. - On
committed: falseno result carriesdata. Errors andfieldsremain.
Limits
MaxBatchSize = 100items per request. Go over that and you get400 Bad Requestbefore any item runs.items(orids) can't be empty.- A batch update item names its record by every key column, under the
column's wire name. An item that omits one answers
missing <key>withreason: "missing_key". Two keys in one item that name the same column (bodyTextandbody_texton a camelCase handler) refuse the batch with 400 before it runs, naming both spellings. For a composite key, a batch delete'sidsareentity.JoinKeystrings ("s1/c1").
Behaviour & guarantees
- Atomic. One transaction; one commit or one rollback.
- Hooks run inside the transaction.
BeforeCreate,AfterUpdate, etc. fire per item. A hook error rolls back the whole batch. - Permission is checked per item. With an
Exposure.Accessblock, the route checks the op's permission once up front, thenPATCHandDELETEask again for every item withRef{Type: <entity>, ID: <item id>}, so a resource-aware Decider that denies one record refuses it here as it does onPATCH /<entity>/{id}. A refused item reports"error": "access denied"and the batch rolls back. - Events fire only on commit.
entity.createdetc. fire after a successful commit, in input order, one per item. They never fire on rollback. - No partial success. If you need "skip failures and keep the
rest", make
MaxBatchSizeindividual calls instead.
Examples
curl -X POST http://localhost:8080/posts/_batch \ -H 'Content-Type: application/json' \ -d '{"items":[ {"title":"First"}, {"title":"Second"}, {"title":"Third"} ]}' curl -X PATCH http://localhost:8080/posts/_batch \ -H 'Content-Type: application/json' \ -d '{"items":[ {"id":"p1","status":"published"}, {"id":"p2","status":"archived"} ]}' curl -X DELETE http://localhost:8080/posts/_batch \ -H 'Content-Type: application/json' \ -d '{"ids":["p1","p2","p3"]}'
Common mistakes
- Treating a non-committed response as a 200. It's a 400. Check
committedbefore you trustresults[*].data. - Counting on event ordering across batches. Within a batch, events fire in input order. Across overlapping batches, order depends on transaction commit order, so don't count on it.
- Sending more than 100 items. Split it client-side.
- Mixing batch and per-item requests in a saga. Mixing makes rollback semantics ambiguous. Pick one.