Batch
Developer·4 minutes to read
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.
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 three resources a script builds with (bots, routines and memories), plus an invitation and a run:
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> in input.botId (on routine.create, memory.create and run.create) — 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_URL/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_URL}/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 could not be validated.",
"details": [{ "path": "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. An unknown op or a malformed item is that item's invalid_request; an invitation.create under a key below admin is that item's forbidden; a run.create refused for payment is that item's payment_required. |
| 400 | invalid_request | More than fifty operations, an empty list, a body that is not { "operations": [...] }, or no Idempotency-Key. This is refused 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 (an admin key when the batch invites anybody). |
| 413 | payload_too_large | The body is over 1 MB. | Send fewer operations, or shorter instructions. |
| 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.