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;nullclears the cap. This is the HTTP twin of the runtime'spipeline_set_budgettool — 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 paused → active. 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
- Workflows — the versioned phase graphs projects expand from.
- Gates & approvals — the human-in-the-loop surface.
- Artifact versioning & backtrack — the append-only artifact model.
- Portal → Projects — the dashboard equivalents.