Events
An event is one row in the runtime's project_events table — the append-only audit trail of a pipeline project advancing. This namespace is the only way to consume these rows — they are not pushed anywhere. If you simply want to be notified when a run or project finishes, that is a different, smaller feature: Webhooks (outbound) delivers run.succeeded, run.failed and project.completed to a URL you register.
Two corrections worth making early, because the shape is not what you'd guess:
- Events are pipeline/project events, not a generic workspace firehose. Every row hangs off a
project_id. - An event
idis an integer. There is noevt_prefix, no dottedkindlikerun.completed, and nosubject/dataenvelope on the row itself.
For the wire-level reference — full field table, the category/level enums, and the list of real topics — see API → Events.
Auth.
GET /v1/eventsrequires the control plane'sx-catentio-sessionheader, which this SDK never sends. NeitherapiKeymode nor Bearer-session mode authenticates against this route. See SDKs → Auth: what actually works.
Namespace
catentio.events — one method:
catentio.events.list(query?)
That's it. Events are immutable: no get, no patch, no delete. There is also no cursor pagination, no kind filter, no since/until bounds, and no per-run or per-project filter on this method — the underlying endpoint (GET /v1/events) simply doesn't take them.
For a single project's timeline, use projects.listEvents(id) instead, which calls GET /v1/projects/{id}/events.
Methods
events.list
Signature. catentio.events.list(query?, token?): Promise<{ data: ProjectEvent[]; meta: Meta }>
Calls GET /v1/events. Returns newest-first. Note the return is the list envelope, not a bare array — the rows are under data.
Query parameters — these four, and only these four:
| Field | Type | Notes |
|---|---|---|
limit |
integer | 1–2000, default 500. |
since_hours |
integer | 0–720, default 24. 0 means no time bound. |
category |
string | One of project, task, subtask, review, gate, artifact, retry, cost, crash. |
level |
string | One of debug, info, warn, error. |
import { CatentioSaasClient } from '@forjio/catentio-saas-node';
const catentio = new CatentioSaasClient({ apiKey: process.env.CATENTIO_API_KEY! });
const { data, meta } = await catentio.events.list({
category: 'gate',
level: 'warn',
since_hours: 6,
limit: 200,
});
console.log(`${data.length} of ${meta.total} matching events`);
for (const e of data) {
console.log(e.ts, e.topic, e.project_id, e.payload_jsonb);
}
Types
The client returns parsed JSON; the row shape is:
interface ProjectEvent {
id: number; // integer row id — not 'evt_...'
ts: string; // ISO 8601
project_id: string;
task_id: string | null; // UUID
subtask_id: string | null; // UUID
attempt_number: number | null;
level: 'debug' | 'info' | 'warn' | 'error';
category:
| 'project' | 'task' | 'subtask' | 'review' | 'gate'
| 'artifact' | 'retry' | 'cost' | 'crash';
topic: string; // 'gate_awaiting', 'project_started', ... (underscored)
actor_kind: string; // 'system' | 'agent' | 'action' | 'human' | 'external'
actor_id: string | null;
payload_jsonb: unknown; // topic-specific detail
trace_id: string | null; // UUID
}
interface Meta {
limit: number;
since_hours: number;
total: number; // count ignoring `limit` — tells you if you truncated
}
payload_jsonb varies by topic. There is no per-kind type export — treat it as unknown and narrow on topic.
Common patterns
Find the projects waiting on a gate
const { data } = await catentio.events.list({ category: 'gate', since_hours: 168, limit: 2000 });
const awaiting = new Set<string>();
for (const e of data.slice().reverse()) { // oldest → newest
if (e.topic === 'gate_awaiting') awaiting.add(e.project_id);
if (e.topic === 'gate_resolved') awaiting.delete(e.project_id);
}
console.log([...awaiting]);
Poll for failures
There's no watermark parameter, so poll on a window and dedupe on the integer id:
const seen = new Set<number>();
async function pollErrors(onEvent: (e: ProjectEvent) => void) {
for (;;) {
const { data } = await catentio.events.list({ level: 'error', since_hours: 1, limit: 500 });
for (const e of data.slice().reverse()) { // oldest → newest
if (seen.has(e.id)) continue;
seen.add(e.id);
onEvent(e);
}
await new Promise(r => setTimeout(r, 5000));
}
}
Check whether you truncated
const { data, meta } = await catentio.events.list({ since_hours: 24, limit: 2000 });
if (meta.total > data.length) {
console.warn(`truncated: ${meta.total - data.length} more events in window`);
}
limit maxes out at 2000. To go deeper, narrow since_hours and walk the window in slices — there is no cursor.
One project's timeline
const { data } = await catentio.projects.listEvents('spring-lookbook-a1b2c3d4');
for (const e of data) console.log(e.ts, e.topic);
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 for failures — you get CatentioError with a generic HTTP_ERROR code and the HTTP reason phrase as the message; the raw detail value lands in .details.
| Status | Cause |
|---|---|
| 401 | Missing/invalid session — this route requires x-catentio-session, which this SDK never sends. |
| 422 | limit or since_hours out of range. |
| 502 | The control plane couldn't reach the runtime. |
Queries that match nothing return an empty data array, not a 404. There is no per-key scope concept in v1 — scopes are accepted-and-ignored everywhere they appear, not just here.
Next
- API → Events — HTTP-level reference, full topic list.
- Gates — what
gate_awaiting/gate_resolvedmean. - Projects —
projects.listEventsfor one project's timeline. - Webhooks — webhook gates (inbound), not event delivery.