Projects

A project is a long-running, pipeline-driven workflow. Where a run is one prompt→reply cycle, a project is a workflow expanded into tasks (one per phase) and subtasks (agent steps, actions, gates), each subtask accumulating attempts, each phase promoting its output to a versioned artifact. The whole thing can be paused, resumed, retried, retriggered, reverted, and abandoned.

The runtime drives projects; the CP proxies the read/write surface (and forces ownership from your verified session) so the portal and API callers never bypass the runtime ↔ control-plane wire.

Object model

Project
  ├── Tasks (one per phase)
  │     └── Subtasks (agent | action | gate steps)
  │           └── Attempts   ←  every execution, retries included
  ├── Attachments   ←  reference files the user uploaded
  ├── Artifacts     ←  versioned per-phase outputs (current + superseded)
  └── Events        ←  full pipeline timeline

The project object

{
  "id": "sustainable-urban-farming-explainer-3f9a1c2b",
  "title": "Sustainable urban farming explainer",
  "workflow_slug": "content",
  "workflow_version": 2,
  "status": "active",
  "has_stale_phases": false,
  "brief": "A practical explainer on sustainable urban farming…",
  "owner_kind": "internal",
  "owner_id": "internal",
  "auth_mode": "oauth",
  "anthropic_workspace_id": null,
  "cost_budget_usd": "5.00",
  "cost_warning_threshold": "0.80",
  "cost_total_usd": "1.8421",
  "cost_burst_per_minute_usd": null,
  "started_at": "2026-07-11T14:00:00Z",
  "completed_at": null,
  "abandoned_at": null
}
Field Type Notes
id string Human-readable: a slug derived from the title plus a random hex suffix. Not a prj_ prefix.
title string Display label. Mutable via PATCH.
workflow_slug, workflow_version string, int The workflow pin. Set at expansion, never changes — see Workflows.
status enum planning, active, paused, completed, abandoned.
has_stale_phases bool True when a retrigger/revert left downstream phases stale under a lazy cascade.
brief string Free-form string (not an object). Immutable after create — it feeds every step's prompt.
owner_kind, owner_id string Force-set from the verified session — you can't spawn projects in other workspaces.
cost_budget_usd decimal string | null Hard cap in USD. Mutable via PATCH.
cost_warning_threshold decimal string Soft-warning fraction of the budget (default 0.80).
cost_total_usd decimal string Running spend across all attempts.

Endpoints

Create a project

POST /v1/projects

Spawn a fresh project. The portal's New project sheet POSTs here. The CP forces owner_kind = "internal" and owner_id = <your customer id>. Workflow expansion happens atomically with creation — the response is a fully planned project.

Body:

{
  "title": "Sustainable urban farming explainer",
  "workflow_slug": "content",
  "brief": "A practical explainer on sustainable urban farming…",
  "workflow_version": 2,
  "cost_budget_usd": "5.00"
}
Field Required Notes
title yes The project id is slugified from it once, at create.
workflow_slug yes Must resolve to an active workflow; archived slugs are rejected with 400.
brief yes Plain string.
workflow_version no Integer ≥ 1 to pin a version; omitted = latest active.
cost_budget_usd no USD decimal (string or number).

Every agent step's tool/skill bindings are validated before any row is created; a workflow with unresolvable bindings fails with 400 template has unresolvable step bindings: ….

const project = await catentio.projects.create({
  title: 'Sustainable urban farming explainer',
  workflow_slug: 'content',
  brief: 'A practical explainer on sustainable urban farming…',
});
project = catentio.projects.create({
    "title": "Sustainable urban farming explainer",
    "workflow_slug": "content",
    "brief": "A practical explainer on sustainable urban farming…",
})
curl -X POST 'https://catent.io/api/v1/cp/v1/projects' \
  -H "Cookie: catentio_session=<session cookie set at login>" \
  -H "Content-Type: application/json" \
  -d '{"title":"Sustainable urban farming explainer","workflow_slug":"content","brief":"…"}'

Update a project

PATCH /v1/projects/{project_id}

Only two fields are mutable, whitelisted at both the CP and the runtime:

  • title — display label only (the id never changes).
  • cost_budget_usd — the budget knob; null clears the cap. This is the HTTP twin of the runtime's pipeline_set_budget tool — the standard hard-cap recovery is raise budget + resume.

brief is deliberately not mutable: it is baked into the prompt every subtask attempt receives. Scope changes go through edit sessions.

List projects

GET /v1/projects

Query params: status, workflow_slug, owner_kind, owner_id, limit (1–500, default 50), offset. Returns { "data": [...], "meta": { "total", "limit", "offset" } }.

Retrieve a project

GET /v1/projects/{project_id}

The full project object. 404 when the id doesn't exist.

List tasks

GET /v1/projects/{project_id}/tasks

One task per phase, with phase_slug, status (pending, running, blocked, review, succeeded, failed, skipped, stale), depends_on, task_kind (regular or edit_session), and per-task cost fields.

Retrieve a task

GET /v1/projects/{project_id}/tasks/{task_id}

List subtasks

GET /v1/projects/{project_id}/tasks/{task_id}/subtasks

Each subtask is one step: kind (agent / action / gate), role (producer / reviewer / tester / assembler / gatekeeper / utility), assignee, per-step tools / skills bindings, output_schema, status, and cost.

Retrieve a subtask

GET /v1/projects/{project_id}/tasks/{task_id}/subtasks/{subtask_id}

List attempts

GET /v1/projects/{project_id}/subtasks/{subtask_id}/attempts

Every execution of a subtask is an attempt with attempt_number, attempt_reason (initial, manual_retry, review_failure, cascade_retrigger, edit_session, …), status (pending / running / succeeded / failed), output, error, cost_usd, duration_ms, and — for agent steps — the child_run_id of the agent run it spawned.

List events

GET /v1/projects/{project_id}/events?limit=200

Full project event timeline: state transitions, gate resolutions, artifact promotions, retries, cost warnings. limit 1–1000, default 200.

Subtask events

GET /v1/projects/{project_id}/subtasks/{subtask_id}/events?limit=200

Per-subtask event stream. limit 1–2000.

Artifacts

GET /v1/projects/{project_id}/artifacts?current_only=true

The project's per-phase pipeline artifacts. Artifacts are versioned and append-only: each phase re-run promotes a new version and marks the previous one superseded: true. current_only=true (the default) returns only each phase's current version; pass current_only=false for the full version history. See Artifact versioning & backtrack.

Attachments

GET  /v1/projects/{project_id}/attachments
POST /v1/projects/{project_id}/attachments   (multipart/form-data)

Reference files the user uploaded. POST accepts one or more files; the CP streams the multipart payload through to the runtime.

curl -X POST 'https://catent.io/api/v1/cp/v1/projects/<project-id>/attachments' \
  -H "Cookie: catentio_session=<session cookie set at login>" \
  -F "files=@brief.pdf" \
  -F "files=@mood-board.png"

Cost rollup

GET /v1/projects/{project_id}/cost

Per-phase cost rollup. Amounts are USD decimal strings:

{
  "project_id": "sustainable-urban-farming-explainer-3f9a1c2b",
  "total_usd": "1.8421",
  "budget_usd": "5.00",
  "budget_burn": 0.37,
  "tasks": [
    { "task_id": "…", "phase_slug": "research", "task_kind": "regular",
      "budget_usd": null, "spent_usd": "0.4210" }
  ]
}

Actions

Pause

POST /v1/projects/{project_id}/pause

Parks the project at status = paused. The autodrive loop skips paused projects. Returns { "project_id", "status": "paused" }.

Resume

POST /v1/projects/{project_id}/resume

Runs crash recovery (re-queues attempts that died mid-flight), then flips pausedactive. Autodrive picks the project up on its next tick. Returns the recovery summary (crashed_attempts_recovered, repeat_crash_failures, nonidempotent_pauses).

Abandon

POST /v1/projects/{project_id}/abandon

Terminal — flip to status = abandoned. Artifacts are kept. Cannot be undone via the API.

Run one step

POST /v1/projects/{project_id}/run-step

Advance the runner by exactly one eligible subtask. Returns { "ran_step": false, "message": "no eligible subtask" } when nothing is dispatchable, otherwise the attempt outcome (subtask_id, status, attempt_number, attempt_reason, output_validated, error, duration_ms). With autodrive running you rarely need this — it exists for manual stepping.

Retry a subtask

POST /v1/projects/{project_id}/subtasks/{subtask_id}/retry

Queue a manual_retry attempt for a failed subtask. Optional addendum_brief query param appends guidance to the original prompt (“redo this, but make the tone less corporate”). The subtask resets to pending, the failed parent task re-enters the DAG frontier, and the runner picks it up on the next tick.

Retries don't stack: if an attempt is already queued you get 409 attempt N is already queued for this subtask.

Resolve a human gate

POST /v1/projects/{project_id}/subtasks/{subtask_id}/resolve-gate

Approve or reject a blocked human gate. Body:

{ "decision": "approved", "comment": "Storyboard looks right." }

decision is approved or rejected — nothing else. decided_by is always derived from your verified session (the CP sends portal:<huudis-user-id>); the body cannot spoof it.

Semantics, response shape, and gate kinds are documented on Gates & approvals. Errors: 400 invalid decision, 404 subtask not in project, 409 when the subtask isn't a blocked human gate (or you lost a resolution race).

Backtrack

Shipped with the portal's backtrack surface; all three refuse to touch in-flight phases.

Retrigger a phase

POST /v1/projects/{project_id}/phases/{phase_slug}/retrigger

Re-run a terminal phase (succeeded / failed / skipped / stale). Body (all optional):

{ "addendum_brief": "Tighten the hook in scene 1.", "cascade": "lazy" }

cascade is one of lazy (default — downstream phases marked stale, not re-run), strict (auto-retrigger downstream), none (pin downstream against the old output), default (each phase's own template setting). Response:

{
  "project_id": "…", "phase_slug": "storyboard", "cascade": "lazy",
  "affected_phase_slugs": ["scenes", "assemble"],
  "marked_stale": ["scenes", "assemble"],
  "auto_retriggered": [],
  "cost_estimate_usd": "0.6100"
}

409 when the phase is pending / running / blocked / review — a mid-flight phase is already queued or executing, and resetting it would stomp in-flight attempts. 400 bad cascade, 404 unknown project/phase.

Revert an artifact

POST /v1/projects/{project_id}/phases/{phase_slug}/revert-artifact

Flip the phase's current-artifact pointer back to a prior version. Append-only — newer versions stay in history. Body:

{ "target_version": 2, "cascade": "lazy", "reason": "v3 lost the vintage filter" }

Returns the now-current artifact plus the cascade applied. 409 when the phase is running / blocked / review (an in-flight promote would race the pointer flip) or when the target version is already current; 404 unknown version; 400 bad target_version/cascade.

Edit sessions

POST /v1/projects/{project_id}/edit-sessions
POST /v1/projects/{project_id}/edit-sessions/{task_id}/approve

An edit session is the scope-change mechanism: an appended edit_session task that plans a change from a natural-language instruction, gates the plan on your approval, then enacts it (retriggers/reverts with the right cascade).

Create body: { "instruction": "Swap scene 3's location to a rooftop garden", "cascade_mode": "lazy" } — 201 with the spawned task_id plus the planner/gate/enact subtask ids.

Approve body: { "decision": "approved" } where decision is one of approved, rejected, request_changes, edit_plan (the last with a revised_plan object). 409 when the plan gate is already resolved; 404 when the task isn't an edit-session task in this project.

States

planning ──expand+first step──► active ──pause/budget/reject──► paused ──resume──► active
                                  │                                                │
                                  └────────► completed                             │
                                  └────────► abandoned (terminal) ◄────────────────┘
State Means
planning Created; template expanded, first step not yet dispatched.
active Autodrive is (or will be) executing subtasks.
paused Idle until /resume — operator pause, budget breach, or a standalone gate rejection.
completed Final phase finished cleanly.
abandoned Operator pulled the plug. Terminal.

There is no running / failed / succeeded project status. A subtask that exhausts its retry budget fails its task, and the project pauses for a human rather than dying.

Errors

Errors are FastAPI-shaped: { "detail": "…" } with the HTTP status carrying the class. The common ones:

Status When
400 Missing title / workflow_slug / brief; unknown or archived template; unresolvable step bindings; immutable field in PATCH; bad cascade / decision / target_version.
404 Project / task / subtask / phase / artifact version doesn't exist.
409 Retry while an attempt is already queued; retrigger on a non-terminal phase; revert on an in-flight phase or to the already-current version; gate already resolved.
502 CP couldn't reach the runtime.

Events & webhook gates

Projects write internal project_events rows as they advance (topics such as project_started and project_completed). These are database rows for audit and observability — Catentio does not push them to any endpoint you register. Query them with GET /v1/events.

If a project's template declares a webhook gate, the project blocks at that step until an external system calls back to resolve it.

Next