Scheduled jobs

GET /v1/scheduled-jobs returns three kinds of job in one list, not just user-created ones:

kind Origin Editable via this API?
system Code-defined singleton jobs (housekeeping sweeps, etc). enabled/cron only, via PATCH — never created/deleted here.
agent Code-defined jobs that fire an agent run (DISCRETE_JOBS). enabled/cron only, via PATCH — never created/deleted here.
custom Portal-created jobs: {name, cron, agent_slug, brief}. Full CRUD.

Only custom jobs are POST/DELETE-able. Use them to wake an agent on a schedule — a daily PR-status DM, a weekly pipeline kickoff, and so on. The runtime owns the cron evaluator; the CP is a thin proxy.

The scheduled job object

A system/agent row:

{
  "id": "kaneko_daily_digest",
  "kind": "agent",
  "agent": "kaneko",
  "schedule": "hour=9 minute=0",
  "enabled": true,
  "next_run_at": "2026-05-13T09:00:00+00:00",
  "description": "Summarise yesterday's activity to Discord."
}

A custom row adds brief, name, model, reasoning:

{
  "id": "custom-14",
  "kind": "custom",
  "agent": "hachimi",
  "schedule": "0 9 * * *",
  "enabled": true,
  "next_run_at": "2026-05-13T09:00:00+00:00",
  "description": "Read open PRs across forjio family. Summarise red CI to Disc",
  "brief": "Read open PRs across forjio family. Summarise red CI to Discord.",
  "name": "Daily PR status",
  "model": "auto",
  "reasoning": null
}
Field Type Notes
id string No sj_ prefix. Built-in (system/agent) jobs use their bare code-defined job id (e.g. kaneko_daily_digest). Custom jobs use custom-<row id>, where <row id> is a plain autoincrement integer.
kind enum system, agent, or custom — see the table above.
agent string | null The agent slug the job fires, when applicable. null for pure system jobs.
schedule string For custom jobs, the raw cron expression. For built-ins, either an operator override cron string or a human-readable rendering of the code default's fields (there is no clean "get the cron string back" accessor for a code-defined apscheduler trigger).
enabled bool If false, this job doesn't fire on its next tick.
next_run_at timestamp | null Pre-computed next firing time in UTC. null if it can't be computed.
description string Built-ins: the job function's docstring first line. Custom: brief truncated to 200 chars.
brief string Custom-only. The full, untruncated prompt sent as the run's message — use this field to edit, not description.
name string Custom-only.
model string Custom-only. A pinned model id, or "auto" (resolver decides — the default).
reasoning bool | null Custom-only. true/false pins the reasoning rider; null means "auto" (derived from the resolved model's capability).

There is no tz field. Cron is always evaluated in UTC — CronTrigger.from_crontab(..., timezone="UTC") is hardcoded, with no per-job timezone override exposed anywhere in this API. Convert your desired local time to UTC yourself. There is also no last_run_at/last_run_id field on this endpoint.

Endpoints

List jobs

GET /v1/scheduled-jobs

Powers Dashboard → Scheduled jobs. Meta: {"total", "heartbeat_total", "heartbeat_enabled"} — the last two are a summary tile linking out to the separate heartbeats page.

const { data } = await catentio.scheduledJobs.list();
curl 'https://catent.io/api/v1/cp/v1/scheduled-jobs' \
  -H "Cookie: catentio_session=<session cookie set at login>"

Create a job

POST /v1/scheduled-jobs

Always creates a custom job. Body:

{
  "name": "Daily PR status",
  "agent_slug": "hachimi",
  "cron": "0 9 * * *",
  "brief": "Read open PRs across forjio family. Summarise red CI to Discord.",
  "enabled": true
}

name, cron, agent_slug, and brief are required (400 if any is missing/blank — note the field is agent_slug, not agent). cron must parse as a standard 5-field expression (validated against UTC; there's no separate timezone to get wrong). enabled defaults to true. Optional model (a model id, or "auto"/omitted) and reasoning (true/false, or omitted for auto) pin per-tick overrides.

agent_slug isn't checked against the real agent registry. A typo'd slug is accepted at create time and only fails later, when the job actually fires and tries to dispatch a run.

Response: {"ok": true, "id": "custom-14", "name", "cron", "agent_slug", "brief", "enabled", "model", "reasoning", "requires_scheduler_restart": false}.

Update a job

PATCH /v1/scheduled-jobs/{job_id}

Partial update. Patchable fields depend on the job:

  • Any job (system/agent/custom): enabled, cron.
  • Custom jobs only: brief, name, model, reasoning. model/reasoning use omit-vs-null semantics — a key that's simply absent leaves the current value unchanged, while an explicit null clears it back to "auto".

The CP rejects an empty body with 400 provide at least one of: enabled, cron, brief, name, model, reasoning.

await catentio.scheduledJobs.update('custom-14', { enabled: false });
curl -X PATCH 'https://catent.io/api/v1/cp/v1/scheduled-jobs/custom-14' \
  -H "Cookie: catentio_session=<session cookie set at login>" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'

Delete a job

DELETE /v1/scheduled-jobs/{job_id}

Only custom jobs are deletable. A system/agent job id returns 400 — disable it via PATCH {"enabled": false} instead; it can't be removed through this API at all.

Attachments

POST /v1/scheduled-jobs/{job_id}/attachments   (multipart/form-data)
GET  /v1/scheduled-jobs/{job_id}/attachments

Custom jobs only (400 on a built-in job id). Upload reference files the agent should read when the job runs. Max 10 files, 25 MB total per upload. Same shape as project attachments — see Projects.

Cron evaluator

The runtime runs a single cron tick every minute, in UTC. On each tick it fires every enabled job whose schedule matches, computing the next next_run_at. A job that's "overdue" (e.g., the runtime was restarted across multiple firing windows) runs once at the next tick — missed windows are not back-filled.

Each scheduled trigger creates a normal run with transport = "cron". You can inspect them via GET /v1/runs?transport=cron just like any other run.

Failure handling

A scheduled-job run that fails doesn't retry automatically. The job stays scheduled and tries again on the next cron tick.

There is no job-level failure notification. But a scheduled job's runs are ordinary runs, so an outbound webhook on run.failed will fire for them — see Webhooks (outbound). To find out that a scheduled run failed, poll: list runs with transport=cron and check their status, or poll GET /v1/events for the pipeline timeline.

Errors

Status When
400 empty_patch-equivalent (provide at least one of: ...); missing name/cron/agent_slug/brief on create; malformed cron/enabled/brief/name; deleting or otherwise mutating a field a built-in job doesn't support.
404 Job id doesn't exist.
409 A custom job with that name already exists (unique per workspace).
502 CP couldn't reach the runtime.

There is no 422 on this resource — cron/validation failures come back as 400, and there is no invalid_tz/unknown_agent error code (there's no tz field, and agent_slug isn't validated against the agent registry — see the callout above).

Events

Scheduled-job CRUD isn't emitted as events. Triggered runs write standard run events (run_started, run_completed, run_failed) with transport = "cron". Read them via GET /v1/events — Catentio does not push them to your endpoints.

Heartbeats vs. scheduled jobs

Catentio has a separate /v1/heartbeats namespace for fixed-interval triggers (e.g., "fire every 5 minutes"). They differ from scheduled jobs:

Scheduled jobs Heartbeats
Granularity Cron expression, UTC Fixed interval (seconds)
Use case "Daily at 9am UTC" "Every 5 minutes, irrespective of clock"
Drift Locked to cron Drifts with runtime restarts

For most "wake an agent on a schedule" use cases scheduled jobs are the right pick. Heartbeats are for low-jitter monitoring loops.

Next

  • Runs — the run rows spawned by transport = "cron".
  • Agents — what the agent_slug field references.
  • Events — poll here to detect scheduled-run failures.