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 id is an integer. There is no evt_ prefix, no dotted kind like run.completed, and no subject/data envelope 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/events requires the control plane's x-catentio-session header, which this SDK never sends. Neither apiKey mode 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_resolved mean.
  • Projectsprojects.listEvents for one project's timeline.
  • Webhooks — webhook gates (inbound), not event delivery.