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
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The credential is missing, refused, or below the scope the route needs. | Use a write key for the writes. |
| 400 | invalid_request | The 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. |
| 403 | forbidden | The bot in botId is not one this key's person owns. | Use the bot owner's key. |
| 404 | not_found | No such routine, or no such trigger on that routine. | List first. |
| 429 | quota_exceeded | The bot already has 50 routines. | Delete one of that bot's routines first. |
| 429 | rate_limited | A 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.