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 idruns.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-session header, which this SDK never sends. Neither apiKey mode 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 is GET /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, no prompt, no ended_at, no duration_ms, no token counts, no cost_idr, no error_code/error_message, and no embedded events array. 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.invoke isn'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).