Batch
Developer·4 minutes to read
Verified against docs/superpowers/specs/2026-09-15-p4-a-typed-http-api-openapi-webhooks.md §4.1, §4.3, §4.4, §6; re-verified against the code at close-out.
What it does
One request carries up to fifty operations, runs them in order inside one transaction, and either commits all of them or commits none (F-DEV-5 — the typed HTTP contract's batch form).
Authentication
An organisation API key of scope write or wider. One operation needs more than that: an invitation.create item needs admin, and the check is per item, so a write key sending a batch that contains one is refused on that item and the whole batch rolls back. See Authentication and keys.
Request
POST /batch
<!-- generated:begin POST /batch --> <!-- generated:end POST /batch -->Eleven operations may appear in a batch, and they are the create, update and delete verbs of the four resources a script builds with, plus the two that start work:
bot.create · bot.update · bot.delete · routine.create · routine.update · routine.delete · memory.create · memory.update · memory.delete · invitation.create · run.create
Keys, webhook endpoints and organisations are not among them: a credential is never created in a batch.
Each item may carry a ref of up to 40 characters. A later item names an earlier item's result with $ref:<ref> wherever an id is expected — so a bot and its routine are created in one call, before the bot's id exists anywhere outside the request. A $ref to a later item, or to one that failed, is invalid_request for the item that used it.
The whole batch is one idempotency row, so a retry with the same Idempotency-Key and the same body replays the first answer rather than running fifty operations twice.
curl -sS -X POST "$BOTSEON_API_ORIGIN/api/v1/batch" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "idempotency-key: 8d9e0f1a-2b3c-4d5e-8f9a-0b1c2d3e4f5a" \
-H "content-type: application/json" \
-d '{"operations":[{"op":"bot.create","ref":"triage","input":{"from":"template","templateId":"inbox-triage","name":"Inbox Triage"}},{"op":"routine.create","ref":"morning","input":{"botId":"$ref:triage","name":"Morning triage","instruction":"Sort the overnight mail.","triggers":[{"kind":"schedule","cron":"0 7 * * 1-5","timezone":"Europe/Copenhagen"}]}}]}'
const res = await fetch(`${process.env.BOTSEON_API_ORIGIN}/api/v1/batch`, {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.BOTSEON_API_KEY}`,
'idempotency-key': crypto.randomUUID(),
'content-type': 'application/json',
},
body: JSON.stringify({
operations: [
{
op: 'bot.create',
ref: 'triage',
input: { from: 'template', templateId: 'inbox-triage', name: 'Inbox Triage' },
},
],
}),
});
const result = (await res.json()) as {
ok: boolean;
results: Array<{ ok: boolean; ref: string | null; id?: string | null }>;
};
if (!result.ok) throw new Error('batch rolled back');
for (const item of result.results) console.log(item.ref, item.id);
Response
A batch that succeeds answers 200 with ok: true and one result per operation, in order:
{
"ok": true,
"results": [
{ "ok": true, "ref": "triage", "id": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c" },
{ "ok": true, "ref": "morning", "id": "5d6e7f80-1a2b-4c3d-9e0f-1a2b3c4d5e6f" }
]
}
A batch that fails answers 422 with ok: false and the same list — every operation is still evaluated, so one round trip tells you every error rather than the first one:
{
"ok": false,
"results": [
{ "ok": true, "ref": "triage", "id": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c" },
{
"ok": false,
"ref": "morning",
"error": {
"code": "invalid_request",
"message": "The request was not accepted.",
"details": [{ "path": "triggers.0.cron", "message": "Invalid cron expression" }]
}
}
]
}
The id on a successful item in a failed batch is the id that operation would have had. Nothing was committed.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 422 | — | One or more items failed. The status is 422 and the body is the result list above; there is no envelope, because the failure is per item. | Read results, fix the items that failed, and send the batch again with a new Idempotency-Key. |
| 400 | invalid_request | More than fifty operations, an empty list, an unknown op, or no Idempotency-Key. This is refused at the schema, before any transaction opens. | Split the work into batches of fifty or fewer. |
| 401 | unauthenticated | The credential is missing, refused, or below write. | Use a write key, and an admin key when the batch invites anybody. |
| 413 | payload_too_large | The body is over 1 MB. | Send fewer operations, or shorter instructions. |
| 402 | payment_required | The organisation needs a trial or a payment. | Settle the account, then retry. |
| 429 | rate_limited | A bucket is full. | Wait for Retry-After. |
Rate limits
A batch is one request against the two one-minute buckets, 300 per key and 1 000 per organisation, however many operations it carries — which is the reason to prefer it over fifty separate calls. The operations inside it still meet their own product ceilings: fifty operations is the batch's limit, and a run.create inside a batch counts against the organisation's run starts for the hour exactly as a direct one does. A full bucket answers rate_limited with Retry-After: 60; wait that many seconds and retry. Failed authentication is counted separately, against the caller's address, so a working key's allowance is never spent by somebody else's bad credential. See Envelopes, errors, idempotency and paging.
Last verified against build c0f77aa.