Runs
A run is one dispatch of an agent through the runtime — created by POST /v1/runs (which this SDK has no method for; see agents.invoke for why that's not a substitute). This namespace can list runs and cancel one, but cannot fetch a single run by id — runs.get() targets a route that doesn't exist.
For the wire-level reference and the real event vocabulary, see API → Runs.
Auth. Every route in this namespace requires the control plane's
x-catentio-sessionheader, which this SDK never sends. NeitherapiKeymode nor Bearer-session mode authenticates here. See SDKs → Auth: what actually works.
Namespace
catentio.runs — three methods:
catentio.runs.list(query?)
catentio.runs.get(id)
catentio.runs.cancel(id)
The control plane also has GET /v1/runs/stats/today, GET /v1/runs/costs, GET /v1/runs/{id}/events, GET /v1/runs/{id}/context(/content), POST /v1/runs/{id}/retry, and GET /v1/runs/{id}/detail — none of them are wrapped by this SDK. There is also no SDK method for POST /v1/runs (starting a run) at all.
Methods
runs.list
Signature. catentio.runs.list(query?, token?): Promise<{ data: RunListRow[]; meta: { total: number; limit: number; offset: number } }>
Rows are aggregated from the events table, newest-activity-first — this is not the same object runs.get would return if it worked. Real query parameters:
| Field | Type | Notes |
|---|---|---|
agent |
string | Filter to one agent slug. |
transport |
string | discord, rest, cli, slack, telegram, whatsapp, gojo. |
since_hours |
integer | 1–720. Omit for no time bound. |
limit |
integer | 1–500, default 50. |
offset |
integer | ≥0, default 0. |
There is no status filter, no since (ISO date), and no before cursor — pagination is limit/offset only. Filter on status client-side using each row's last_status.
import { CatentioSaasClient } from '@forjio/catentio-saas-node';
const catentio = new CatentioSaasClient({ session });
const { data, meta } = await catentio.runs.list({ agent: 'hachimi', limit: 20 });
for (const r of data) {
console.log(r.run_id, r.last_status, r.total_cost_usd);
}
Real row shape:
interface RunListRow {
run_id: string; // 26-char lowercase base32, e.g. 'gk3n7q2fzja4bdm6xr8v1cwt5p' — never 'run_...'
parent_run_id: string | null;
agent: string | null;
transport: string;
started_at: string;
last_event_at: string;
last_event_type: string; // e.g. 'run_completed' — underscored, never 'run.completed'
last_status: string;
event_count: number;
total_cost_usd: string; // stringified decimal, USD
model: string | null;
reasoning: string | null;
model_tier: string | null;
model_rule: string | null;
model_enforce: boolean | null;
}
There is no workspaceId, agentSlug, prompt, workMode, status (it's last_status), output, tokensIn/tokensOut, costUsd (it's total_cost_usd, a string), startedAt/finishedAt (camelCase), embedded events, projectId, createdAt, or updatedAt field on this row.
runs.get — targets a route that does not exist
Signature. catentio.runs.get(id, token?): Promise<Run>
This call fails. It sends
GET /v1/runs/{id}. There is no such route on the control plane — every call 404s, for any id. The closest real thing isGET /v1/runs/{run_id}/detail, which this SDK doesn't wrap and which 404s once the run has aged out of the runtime's in-memory registry. If it worked, its response (RunDetailResponse) would be exactly:interface RunDetailResponse { run_id: string; customer_id: string; transport: string; agent: string | null; parent_run_id: string | null; status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'; // never 'started'/'completed' started_at: string; completed_at: string | null; output: string | null; error: string | null; }That's the whole object — no
work_mode, noprompt, noended_at, noduration_ms, no token counts, nocost_idr, noerror_code/error_message, and no embeddedeventsarray. Call it with the low-level client if you need it:catentio.api.get('/v1/runs/{run_id}/detail').
runs.cancel
Signature. catentio.runs.cancel(id, token?): Promise<{ cancelled: true }>
Sends POST /v1/runs/{id}/cancel. This route is real.
await catentio.runs.cancel('gk3n7q2fzja4bdm6xr8v1cwt5p');
Run status and event vocabulary
Real RunStatus values (app/runtime/run_state.py): queued, running, succeeded, failed, cancelled. There is no started and no completed.
Real event_type vocabulary: run_received, run_started, model_resolved, context_loaded, claude_request, claude_iteration, tool_call, tool_result, run_completed, run_failed, run_cancelled, agent_run_started, user_feedback — all underscored, never dotted (run.completed doesn't exist), and never tool_call_started/tool_call_completed/assistant_message_chunk.
Run events (GET /v1/runs/{id}/events, not wrapped by this SDK — call it via catentio.api.get) are a different table from the pipeline project_events returned by GET /v1/events; the shapes don't interchange. If you called it directly, each row looks like:
interface RunEvent {
id: number; // integer row id — not 'evt_...'
ts: string;
event_type: string;
agent: string | null;
transport: string | null;
duration_ms: number | null;
cost_usd: string | null; // stringified decimal or null
status: string;
payload: unknown; // shape depends on event_type
}
There is no user-facing WebSocket or SSE anywhere in Catentio — ignore any older reference to streaming a run's events live.
Common patterns
Filter by status client-side
There's no server-side status filter, so pull a window and filter on last_status:
const { data } = await catentio.runs.list({ agent: 'sai', limit: 200 });
const running = data.filter(r => r.last_status === 'running');
Batch-cancel by agent
const { data } = await catentio.runs.list({ agent: 'sai', limit: 200 });
const running = data.filter(r => r.last_status === 'running');
await Promise.all(running.map(r => catentio.runs.cancel(r.run_id)));
Page through history with limit/offset
async function* allRuns(agent: string) {
let offset = 0;
const limit = 200;
for (;;) {
const { data, meta } = await catentio.runs.list({ agent, limit, offset });
yield* data;
offset += data.length;
if (offset >= meta.total || data.length === 0) break;
}
}
Errors
The real error body is FastAPI's {"detail": ...}. Because the wire response never has an error key, the SDK doesn't parse out a structured code/message — you get CatentioError with a generic HTTP_ERROR code and the HTTP reason phrase as the message.
| Status | Cause |
|---|---|
| 401 | Missing/invalid session — this SDK never sends x-catentio-session. |
| 404 | Always, for runs.get(). |
| 422 | Bad query filter (e.g. since_hours out of range). |
| 502 | CP couldn't reach the runtime. |
Next
- Agents — why
agents.invokeisn't the way to start a run either. - Projects — the multi-run wrapper taskrunner-mode work creates.
- Events — the separate pipeline-event archive.
- API → Runs — HTTP-level reference, including
POST /v1/runs(the real way to start one).