Runs
Developer·5 minutes to read
What it does
A run is one piece of work a bot does, from the message that started it to the answer it gives: these endpoints start one, list them, read one and cancel one (durable runs with a checkpoint per step, the triggers that start them, the hash-chained ledger, streaming, budgets, the context budget, the failure states, the blocked state and sub-runs).
Authentication
An organisation API key. GET /runs and GET /runs/{id} need scope read or wider; POST /runs and POST /runs/{id}/cancel need write or wider. A run started here is the key's person sending a message to their own conversation with the bot, so it meets the same access and credit gates the app's composer meets, and it carries the same budget: fifty steps, two euros, ten minutes. A phone's send is the same send, and a script's is too. See Authentication and keys.
Request
POST /runs
<!-- generated:begin POST /runs --> <!-- generated:end POST /runs -->GET /runs
<!-- generated:begin GET /runs --> <!-- generated:end GET /runs -->GET /runs/{id}
<!-- generated:begin GET /runs/{id} --> <!-- generated:end GET /runs/{id} -->POST /runs/{id}/cancel
<!-- generated:begin POST /runs/{id}/cancel --> <!-- generated:end POST /runs/{id}/cancel -->GET /runs takes an optional botId and an optional state. POST /runs takes botId and message (1 to 32 000 characters after trimming); it answers 201 with the run it started, the conversation it runs in, and the id and sequence number of the message that started it. When a run is already working in that conversation the message is not queued: the answer is 409 conflict with details.reason run_active, the working run's id in details.runId and Retry-After: 60 — wait for that run to finish, or cancel it, then send again.
Every run started here is tainted from its first token, and its trigger is api: a message that arrives through a key may have come from somebody else's users, so the run loses the capabilities a tainted run may not have, and its memory and creation writes become cards a human must answer. There is no flag to turn this off.
POST /runs/{id}/cancel is idempotent: a run that has already finished answers 200 with its row unchanged and emits no event.
curl -sS -X POST "$BOTSEON_API_URL/api/v1/runs" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "idempotency-key: 2f3a4b5c-6d7e-4f8a-9b0c-1d2e3f4a5b6c" \
-H "content-type: application/json" \
-d '{"botId":"0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c","message":"Draft the release notes for v1.4."}'
The same send from Node, through the SDK, waiting for the answer:
import { createClient } from '@botseon/sdk';
const client = createClient({
baseUrl: process.env.BOTSEON_API_URL ?? '',
apiKey: process.env.BOTSEON_API_KEY ?? '',
});
const result = await client.runTask({
bot: { name: 'Inbox Triage' },
text: 'Draft the release notes for v1.4.',
timeoutMs: 900_000,
});
console.log(result.outcome, result.steps, result.spendMicroCents);
console.log(result.text);
Response
POST /runs answers 201 with the run and where it lives:
{
"run": {
"id": "7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
"botId": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c",
"conversationId": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
"routineId": null,
"state": "queued",
"trigger": "api",
"taint": true,
"steps": 0,
"spendMicroCents": 0,
"error": null,
"pauseReason": null,
"pendingApproval": null,
"createdAt": "2026-09-18T08:00:00.000Z",
"startedAt": null,
"finishedAt": null
},
"conversationId": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
"messageId": "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
"messageSeq": 1
}
GET /runs/{id} and POST /runs/{id}/cancel answer the run object alone; GET /runs answers a page of them under data.
state is one of eight: queued, running, waiting_approval, blocked, paused_limit, completed, failed, cancelled. The last three are the terminal ones, and reaching any of them is what sends a run.finished webhook (Webhooks). waiting_approval means a person has to answer something before the run goes on (Approvals); blocked means the bot stopped rather than improvise; paused_limit means a usage limit was reached and the run stopped; neither blocked nor paused_limit resumes on its own, and the next message starts a fresh run. spendMicroCents is an integer in micro-cents.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The credential is missing, refused, or below the scope the route needs. | Use a write key to start or cancel a run. |
| 404 | not_found | The bot is not visible to this key's person, or it is a team bot whose direct conversation belongs to its owner alone. | Start the run as the bot's owner, or use a bot this person owns. |
| 409 | conflict | A run is already working in this conversation (details.reason is run_active, details.runId names it). | Wait Retry-After seconds, or cancel that run, then send again. |
| 400 | invalid_request | The message is empty or over 32 000 characters, or the POST carries no Idempotency-Key. | Read details. |
| 402 | payment_required | The organisation needs a trial or a payment. The check runs before any row is written. | Settle the account, then retry. |
| 403 | forbidden | The key's person has no access to start runs, or tries to cancel a run in a conversation they can only read. | Use the conversation owner's key. |
| 429 | quota_exceeded | The organisation has made its hour's run starts — 120 by default, and an operator can raise it for one organisation — or the key's service account is at its monthly spend limit. | Wait for the rolling hour to move, or spread the work out. |
| 429 | rate_limited | A request bucket is full. | Wait for Retry-After. |
| 503 | unavailable | The service could not answer. | Retry with backoff. |
Rate limits
Two one-minute request buckets, 300 per key and 1 000 per organisation, and one product ceiling that is not a request bucket at all: an organisation may create 120 run rows in any rolling hour by default, counted at creation, and a routine's own firings are counted separately. 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.