Routines and triggers

Developer·4 minutes to read

What it does

A routine is one bot or one group, an instruction, and one or more triggers; these endpoints create it, read it, change it, delete it, and add or remove a trigger (the routine record, the editor's fields, and the event-trigger menu).

Authentication

An organisation API key. GET needs scope read or wider; the writes need write or wider. A routine runs as the key's person, so the bot it names must be one that person owns. See Authentication and keys.

Request

GET /routines

<!-- generated:begin GET /routines --> <!-- generated:end GET /routines -->

POST /routines

<!-- generated:begin POST /routines --> <!-- generated:end POST /routines -->

GET /routines/{id}

<!-- generated:begin GET /routines/{id} --> <!-- generated:end GET /routines/{id} -->

PATCH /routines/{id}

<!-- generated:begin PATCH /routines/{id} --> <!-- generated:end PATCH /routines/{id} -->

DELETE /routines/{id}

<!-- generated:begin DELETE /routines/{id} --> <!-- generated:end DELETE /routines/{id} -->

POST /routines/{id}/triggers

<!-- generated:begin POST /routines/{id}/triggers --> <!-- generated:end POST /routines/{id}/triggers -->

DELETE /routines/{id}/triggers/{triggerId}

<!-- generated:begin DELETE /routines/{id}/triggers/{triggerId} --> <!-- generated:end DELETE /routines/{id}/triggers/{triggerId} -->

GET /routines requires botId: it lists that bot's routines, and without it the answer is invalid_request naming botId. PATCH /routines/{id} takes at least one of name, instruction, active and notifyOnFinish. A routine's name is 1 to 80 characters and its instruction 1 to 8 000; it is active by default, and a create may carry up to ten triggers in the same call, which are written in one transaction with the routine.

A trigger is one of two shapes. A schedule trigger carries a cron expression and a timezone; the product's cron has a one-minute floor. An event trigger carries a kind from the product's own list and a configuration object, which the repository validates a second time against that kind's own schema — so a configuration the API accepts at the outer schema can still be refused as invalid_request with the failing path named.

curl -sS -X POST "$BOTSEON_API_URL/api/v1/routines" \
  -H "authorization: Bearer $BOTSEON_API_KEY" \
  -H "idempotency-key: 7a2b1c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d" \
  -H "content-type: application/json" \
  -d '{"botId":"0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c","name":"Morning triage","instruction":"Sort the overnight mail and flag anything that needs an answer today.","triggers":[{"kind":"schedule","cron":"0 7 * * 1-5","timezone":"Europe/Copenhagen"}]}'
const res = await fetch(`${process.env.BOTSEON_API_URL}/api/v1/routines?botId=0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c&limit=50`, {
  headers: {
    authorization: `Bearer ${process.env.BOTSEON_API_KEY}`,
    accept: 'application/json',
  },
});
if (!res.ok) throw new Error(`routines: ${res.status}`);
const page = (await res.json()) as {
  data: Array<{ id: string; name: string; active: boolean; nextFireAt: string | null }>;
  nextCursor: string | null;
};
for (const routine of page.data) console.log(routine.name, routine.active, routine.nextFireAt);

Response

{
  "id": "5d6e7f80-1a2b-4c3d-9e0f-1a2b3c4d5e6f",
  "botId": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c",
  "groupConversationId": null,
  "name": "Morning triage",
  "instruction": "Sort the overnight mail and flag anything that needs an answer today.",
  "active": true,
  "notifyOnFinish": true,
  "lastFiredAt": null,
  "nextFireAt": "2026-09-21T05:00:00.000Z",
  "triggers": [
    {
      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "kind": "schedule",
      "cron": "0 7 * * 1-5",
      "timezone": "Europe/Copenhagen",
      "config": {},
      "createdAt": "2026-09-18T08:00:00.000Z"
    }
  ],
  "createdAt": "2026-09-18T08:00:00.000Z"
}

Exactly one of botId and groupConversationId is set: a routine belongs to one bot or to one group. nextFireAt is the scheduler's own next time and is null for a routine with no schedule trigger.

Errors

StatusCodeWhenWhat to do
401unauthenticatedThe credential is missing, refused, or below the scope the route needs.Use a write key for the writes.
400invalid_requestThe cron expression, the timezone or an event trigger's configuration failed validation, a field is outside its bounds, or the POST carries no Idempotency-Key. A schedule with the timezone auto is refused on timezone while the member's own zone is not known yet, so it never runs in UTC instead.Read details for the failing path. For auto, the member sets a zone in Settings → General → Timezone, or opens Botseon in a browser or on the phone.
403forbiddenThe bot in botId is not one this key's person owns.Use the bot owner's key.
404not_foundNo such routine, or no such trigger on that routine.List first.
429quota_exceededThe bot already has 50 routines.Delete one of that bot's routines first.
429rate_limitedA bucket is full.Wait for Retry-After.

Rate limits

Every call on this page is counted against two one-minute buckets: 300 requests per key and 1 000 per organisation. 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.