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

StatusCodeWhenWhat to do
401unauthenticatedThe credential is missing, refused, or below the scope the route needs.Use a write key to start or cancel a run.
404not_foundThe 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.
409conflictA 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.
400invalid_requestThe message is empty or over 32 000 characters, or the POST carries no Idempotency-Key.Read details.
402payment_requiredThe organisation needs a trial or a payment. The check runs before any row is written.Settle the account, then retry.
403forbiddenThe 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.
429quota_exceededThe 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.
429rate_limitedA request bucket is full.Wait for Retry-After.
503unavailableThe 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.