/api/v1 prefix. Workspace routes require a session/JWT and a workspace the
caller belongs to; mutation role checks are listed below. Errors use the API’s
Problem Details shape (error is the human-readable message).
Route inventory
create, manage, and delete are the server’s role-action gates (normally
MANAGER+, OWNER/ADMIN, and OWNER/ADMIN respectively). A missing pipeline is
404; malformed JSON or invalid input is 400; insufficient role is 403;
an unavailable runner/store is 503.
Core endpoint contract
All paths in this page require the/api/v1 prefix. member means an
authenticated session or CLI token with membership in {workspaceId};
create, manage, and delete are the role/capability gates described above.
Requests are JSON unless the operation is a GET or DELETE with no body.
Successful responses are JSON and use the status shown by the operation; an
empty successful delete uses 204 No Content. Invalid JSON, missing required
path/body values, or invalid query values return 400; missing or
soft-deleted resources return 404; authorization failures return 401 or
403; unavailable runner/storage dependencies return 503; unexpected
handler/database failures return 500.
The route-specific request and response contracts are:
The public
POST /api/v1/webhooks/{token} route is the exception to session
authentication: the high-entropy token authenticates dispatch, and an
configured signing secret adds HMAC verification. Accepted dispatches return
202; invalid signatures return 401, rate limits return 429, and an
inactive target returns 409.
Run payload
run and dry_run accept:
tier_override is trivial, fast, moderate, or smart; an unknown value
is treated as no override. triggered_via accepts manual, schedule,
webhook, call_pipeline, or issue; other values fall back to manual.
Omit it for the default manual. run returns the
run result (including run_id, status, output, step_outputs,
duration_ms, and cost_usd). An Idempotency-Key header deduplicates runs.
run_batch takes { "items": [{ "inputs": {}, "tags": [], "metadata": {} }], "tags": [], "tier_override": "fast" } and returns per-item index, run_id,
status, or error.
inputs, tags, and metadata are optional. delay_seconds, ttl_seconds,
debounce_window_seconds, and debounce_max_seconds must be between 0 and
2,592,000 seconds when deferred dispatch is used; invalid values return
400. priority is carried to the pending queue. The Idempotency-Key header
deduplicates a run (the optional idempotency_key_ttl_seconds body field sets
the deduplication window; 0 uses the server default). run_batch requires a
non-empty items array and accepts at most 100 items; violations return 400.
Run statuses and responses
POST .../pipelines/{slug}/run requires member plus create (normally
MANAGER+), and returns 200 OK with a run result for an immediate execution.
The immediate result includes run_id, pipeline_id, pipeline_slug,
status, output, step_outputs, duration_ms, and cost_usd (plus
failure/wait fields when applicable). A deferred request (delay_seconds > 0
or a non-empty debounce_key) returns 202 Accepted with { "status": "SCHEDULED", "pending_id", "fire_at", "coalesced", "priority" } and does
not yet have a run ID.
The run status values are:
SCHEDULED responses use 202 Accepted and contain pending_id, RFC3339Nano
fire_at, priority, and coalesced. WAITING responses also contain the
current step and waitpoint token. DEDUPED may still refer to an in-flight
original. --wait/polling follows WAITING and DEDUPED until the persisted
run reaches a terminal status. Persisted run records additionally use
queued, running, completed, failed, cancelled, interrupted,
dry_run, and waiting (lowercase); interrupted marks boot recovery.
Save and validation
slug and definition are required. Call test_run first with
{ "definition": {...}, "author_crew_id": "crew_123", "sample_inputs": {} };
the successful response contains an HMAC-bound save_token. The legacy
last_test_run_at/last_test_run_passed fields are not proof by themselves.
skip_test_gate and skip_governance_gate are OWNER/ADMIN-only. Risky saves
may have status proposed until approved; only active pipelines run.
Replay, failures, budgets, and state
Budget and state writes require
manage; state keys are limited to 128
characters. The budget response reports month, spent_usd, has_budget,
pct_used, and over_budget where applicable.
Audited operation reference
The following details are the wire contract for the remaining pipeline operations. Every endpoint is prefixed with/api/v1. member means an
authenticated session with membership in {workspaceId}. A request from an
unauthenticated caller returns 401; a caller without workspace membership
returns 403. Problem responses use the API error shape with an error
message. Unless an endpoint says otherwise, an unexpected database/store
failure is 500.
Run changes
GET /api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}/changes
Auth: member plus the read role capability (normally any workspace
member). The run must belong to the workspace; a missing or foreign run is
404 (Run not found). The handler resolves the run’s
invoking_crew_id, falling back to the pipeline’s author_crew_id, and
re-checks that crew’s workspace before contacting the crew container.
Response: 200 OK. If no crew/repository can be resolved, the response is
{ "is_repo": false }. Otherwise the response is the decoded crew git-diff
object (the proxy forwards that object without reshaping it). Failure to reach
the crew proxy is 502; an internal run/crew lookup failure is 500. A
malformed proxy response degrades to { "is_repo": false } with 200.
Run logs
GET /api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}/logs?limit=N
Auth: member plus read. runId is required (400). The run is
workspace-scoped and a missing/foreign run returns 404. limit defaults to
200; a positive value from 1 through 1000 is honored, while an absent,
non-numeric, zero, negative, or larger value leaves the default in place.
Response: 200 OK with an oldest-first JSON array. Each entry is
{ "ts": "RFC3339", "level": "info", "message": "...", "type": "..." };
type is omitted when empty and an empty severity is returned as info.
Journal lookup, row scan, or iteration failures return 500.
Run metadata
PATCH /api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}/metadata
Auth: member plus the update capability (MANAGER+). runId is
required (400). The JSON body must contain at least one non-empty operation
map:
set replaces a key; increment adds numerically (missing keys start at
zero); append pushes onto an array (missing arrays start empty). Invalid JSON
or no operation maps is 400. A missing/foreign run is 404; an unavailable
run store is 503; an update failure is 500.
Response: 200 OK — { "metadata": { ...merged metadata... } }.
POST /api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}/signal
Auth: member plus update (MANAGER+). The body is
{ "event_type": "approval", "payload": "..." }; event_type is required.
Invalid JSON, a missing event_type, or a missing runId is 400. A missing
run store or signal registry is 503; a missing/foreign run is 404. If the
run is not waiting for that event, the response is 404. Durable delivery
records the signal before best-effort in-memory delivery and resumes a parked
run.
Response: 200 OK — { "ok": true, "delivered": true }. A durable
delivery write failure is 500.
GET /api/v1/workspaces/{workspaceId}/pipeline-runs/{runId}/tree
Auth: member. runId is required (400); a missing/foreign run is
404; an unavailable run store is 503; tree loading failure is 500.
Response: 200 OK — { "nodes": [...] }, where each node contains
id, optional parent_id, pipeline_slug, status, triggered_via, and
cost_usd. Nodes are a flat parent-linked run plus descendant list, including
call_pipeline, deferred, and replay descendants.
Pipeline budgets
GET /api/v1/workspaces/{workspaceId}/pipelines/{slug}/budget
Auth: member. A missing/soft-deleted pipeline is 404; an unavailable
run-history store is 503; pipeline lookup or monthly-spend calculation
failure is 500.
Response: 200 OK —
{ "slug", "has_budget", "monthly_budget_usd", "month": "YYYY-MM", "spent_usd", "pct_used"?, "over_budget"? }.
pct_used and over_budget are omitted when no positive budget is set.
Spend is for the current calendar month.
PATCH /api/v1/workspaces/{workspaceId}/pipelines/{slug}/budget
Auth: member plus manage (OWNER/ADMIN). Body:
{ "monthly_budget_usd": 25 }. 0 clears the cap; negative values are
400. Invalid JSON/body read failures are 400. A missing pipeline is 404,
an unavailable run-history store is 503, and persistence or spend
calculation failures are 500.
Response: 200 OK with the same budget object as GET.
GET /api/v1/workspaces/{workspaceId}/pipelines/budget-summary
Auth: member. The response is 200 OK with
{ "month": "YYYY-MM", "routines": [...], "total_budget_usd", "total_spent_usd" }.
Each routine row has slug, monthly_budget_usd, spent_usd, and optional
pct_used/over_budget. Routines with neither a positive budget nor current
month spend are omitted. An unavailable run-history store or list/spend
calculation failure is 503/500, respectively.
Appearance
PATCH /api/v1/workspaces/{workspaceId}/pipelines/{slug}/appearance
Auth: member plus create (OWNER/ADMIN/MEMBER). Picking an icon is a
labelling act rather than an operational one, so it sits at the create tier
rather than manage.
This is deliberately not part of PUT .../pipelines/{slug}. Save rewrites
definition_json, recomputes definition_hash, can mint a new version and
re-runs the governance risk classifier — none of which should happen because
somebody picked a different colour. This endpoint writes two columns.
Request: JSON body { "icon": "receipt", "color": "amber" }. Both fields
are optional and independent:
Values are opaque to the server:
icon is a crew-icon name and color a
gradient-palette id, both owned by the web UI’s icon kit. The server validates
length only — each is bounded at 64 characters — so the kit can gain an icon
without the API rejecting it. An unknown value renders the fallback icon,
which is what an unset value already does. Bodies are read up to 4 KiB.
Response: 200 OK with the updated routine (the same object
GET .../pipelines/{slug} returns, including icon and color). If the write
lands but the follow-up read fails, the response is still 200 OK but carries
only { "icon", "color" } — failing here would have the caller retry a change
that already applied. That is why the response schema is a oneOf.
Statuses: 400 for an unreadable body, invalid JSON, or a value over 64
characters; 401 unauthenticated; 403 below the create tier; 404 when the
routine does not exist in the workspace; 500 on persistence failure.
crewship routine appearance for the CLI equivalent.
Deferred and replay operations
GET /api/v1/workspaces/{workspaceId}/pipelines/pending
Auth: member. 200 OK returns an array (never null) of at most 100
pending deferred triggers. Each item contains id, pipeline_slug, optional
debounce_key, priority, and RFC3339Nano fire_at. An unavailable database
is 503; listing failure is 500.
POST /api/v1/workspaces/{workspaceId}/pipelines/pending/{pendingId}/cancel
Auth: member plus update (MANAGER+). pendingId is required (400).
An unavailable database is 503; a missing, already-fired, expired, or
already-cancelled item is 404; cancellation failure is 500.
Response: 200 OK — { "ok": true, "cancelled": "<pendingId>" }.
POST /api/v1/workspaces/{workspaceId}/pipelines/runs/{runId}/replay
Auth: member plus create (MANAGER+). Body is optional; when present it
is { "pinned_version": 3 }. The replay uses the source run’s original
inputs, metadata, and tags, marks the new run as a replay, and uses HEAD unless
pinned_version is supplied. Invalid JSON or a missing runId is 400.
Response: 200 OK with the newly-created run result. A missing/foreign
source run is 404; a deleted source pipeline is 404; an unavailable run
store is 503; an execution concurrency limit is 429; a pinned version that
does not exist is 409; other lookup/execution failures are 500.
The response is the ordinary RunResult object: run_id, pipeline_id,
pipeline_slug, status, output, step_outputs, duration_ms, and
cost_usd, with failed_at_step/error_message for failures and
waitpoint_token/current_step when the replay parks at a waitpoint. The new
run is marked as a replay of runId; its inputs, metadata, and tags come from
the source run. An omitted pinned_version uses the current head version.
Statuses: 200 OK; 400 for invalid input; 401 without authentication;
403 without membership/create permission; 404 for a missing source;
409 for an invalid pinned version; 429 at the concurrency limit; 503 for
an unavailable run store; 500 for other failures.
GET /api/v1/workspaces/{workspaceId}/pipelines/runs/errors?limit=N
Auth: member. limit is passed to the failure-group query; the default
is 50, and the store normalizes non-positive or greater-than-200 values to
50. The response excludes runs whose pipeline was soft-deleted and groups
failed runs by error_fingerprint, newest group first.
Response: 200 OK — { "groups": [...] }. Each group contains
fingerprint, count, pipeline_slug, failed_at_step, sample_error, and
run_ids. run_ids is a newest-first sample capped at 100 per group; it is
the input for bulk_replay, not necessarily the complete group. An unavailable
run store is 503; failure-group lookup is 500.
POST /api/v1/workspaces/{workspaceId}/pipelines/runs/bulk_replay
Auth: member plus create (MANAGER+). Body must select runs by
run_ids or fingerprint; optional limit defaults to 50 and is clamped
to 50 when absent, non-positive, or greater than 100:
limit. The handler defaults
limit to 50 when it is absent, non-positive, or greater than 100.
No selected IDs is 400; invalid JSON is 400; an unavailable run store or
failure loading fingerprint groups is 503/500. Individual replay failures
are reported per item rather than failing the whole request. The fingerprint
selector expands from the newest sampled IDs returned by the failure-group
store, so a group larger than the sample cannot replay every historical run in
one request.
Statuses: 200 OK; 400 for invalid selectors or JSON; 401 without
authentication; 403 without membership; 503 for an unavailable run store;
500 for failure-group lookup.
Response: 200 OK —
{ "requested": N, "replayed": N, "results": [{ "source_run_id", "new_run_id"?, "status"?, "error"? }] }.
Statuses: 200 OK; 400 for invalid selectors or JSON; 401 without
authentication; 403 without membership/create permission; 503 for an
unavailable run store; 500 for selector or replay failures.
Version diff
GET /api/v1/workspaces/{workspaceId}/pipelines/{slug}/diff?from=N&to=M
Auth: member. Both positive integer query parameters are required
(400). A missing pipeline, from version, or to version is 404;
pipeline/version lookup failures or diff generation failure are 500.
Response: 200 OK —
{ "slug", "from_version", "to_version", "from_hash", "to_hash", "identical", "unified_diff" }.
Definitions are pretty-printed before diffing. unified_diff is empty when
the hashes are identical.
Governance
POST .../pipelines/{slug}/approve and POST .../pipelines/{slug}/reject
take no request body and require create (MANAGER+). Approve changes a
proposed pipeline to active and returns 200 —
{ "slug", "status": "active" }. Reject soft-deletes the proposed pipeline
and returns 200 with the resulting status. A pipeline that is not in the
expected state returns 409; a missing pipeline is 404; other store failures
are 500.
POST .../pipelines/{slug}/disable and /enable take no request body and
require manage (OWNER/ADMIN). Disable returns 200 with
{ "slug", "status": "disabled", "cancelled_runs": N }; enable returns
200 with { "slug", "status": "active" }. They return 404 for a
missing pipeline and 500 for persistence failures. A disabled or proposed
pipeline cannot be run; the run endpoint reports 409 with its current status.
Schedules and public waitpoint completion
Schedule administration is also available through the pipeline API. Create usesPOST .../pipeline-schedules with a JSON body containing cron_expr and
either target_pipeline_slug or target_pipeline_id; it returns 201 with
the schedule object. PATCH .../pipeline-schedules/{scheduleId} merges the
supplied fields into the existing schedule and returns 200 with the schedule
object. Both return 400 for
invalid JSON, missing targets, or invalid cron/timezone data; missing schedules
or targets are 404; backend failures are 500 and an unwired schedule store
is 503. Update requires manage; create requires create. See
Schedules for the request fields and complete
schedule representation.
DELETE .../pipeline-schedules/{scheduleId} requires manage, returns 204
on success, and soft-deletes the schedule. POST .../pipeline-schedules/{scheduleId}/run
requires create, takes no body, and force-fires the stored schedule inputs
without changing its cron cadence. It returns the normal run result (200),
or 404 for a missing/foreign schedule or target, 409 for a missing pinned
version, 429 for the concurrency limit, 500 for execution/storage errors,
and 503 when the schedule store or runner is unavailable.
The public POST /api/v1/waitpoint-tokens/{token} callback has no workspace
session requirement; the high-entropy token authenticates it. Its optional
body is { "approved": true, "payload": <any JSON> }; approved defaults to
true. It returns 200 — { "ok": true, "approved": true|false }, 400 for
an empty token or invalid JSON, 409 when the waitpoint was already decided or
expired, 500 for completion failure, and 503 when the waitpoint backend is
unavailable.
Step overrides
GET /api/v1/workspaces/{workspaceId}/pipelines/{slug}/overrides
Auth: member. A missing pipeline is 404; pipeline or override lookup
failure is 500.
Statuses: 200 OK; 401 without authentication; 403 without
membership; 404 for a missing pipeline or key; 503 for an unavailable state
store; 500 for lookup failure.
Response: 200 OK — { "overrides": [...] }, with each row containing
step_id and optional prompt and model_override.
PUT /api/v1/workspaces/{workspaceId}/pipelines/{slug}/steps/{stepId}/override
Auth: member plus update (MANAGER+). stepId is required (400).
Body is { "prompt": "...", "model_override": "..." }; at least one of the
two non-empty fields is required (400). Invalid JSON is 400. A missing
pipeline or step in the current definition is 404; an invalid stored
definition is 500; save failure is 500.
Response: 200 OK — { "ok": true, "step_id": "<stepId>" }.
DELETE /api/v1/workspaces/{workspaceId}/pipelines/{slug}/steps/{stepId}/override
Auth: member plus update (MANAGER+). The response is 200 OK with
{ "ok": true, "step_id": "<stepId>" }. A missing pipeline is 404; delete
failure is 500. The endpoint is idempotent for an absent override.
Pipeline state
GET /api/v1/workspaces/{workspaceId}/pipelines/{slug}/state?schedule_id=S
Auth: member. Without schedule_id, all schedule buckets are returned;
with it, only that bucket is returned. An empty value selects the shared
manual/webhook bucket. 200 OK returns
{ "slug": "...", "buckets": [{ "schedule_id", "entries": [{ "key", "value", "updated_at" }] }] }.
A missing pipeline is 404; an unavailable state store is 503; lookup
failure is 500.
PUT /api/v1/workspaces/{workspaceId}/pipelines/{slug}/state/{key}
Auth: member plus manage (OWNER/ADMIN). Body is
{ "value": "...", "schedule_id": "..." }; omitted schedule_id means the
manual/webhook bucket. Empty keys or keys longer than 128 characters are
400; invalid JSON is 400. A missing pipeline is 404; an unavailable
state store is 503; write failure is 500.
Response: 200 OK —
{ "slug", "schedule_id", "key", "value" }.
Statuses: 200 OK; 400 for invalid JSON or key; 401 without
authentication; 403 without manage permission; 404 for a missing pipeline;
503 for an unavailable state store; 500 for write failure.
DELETE /api/v1/workspaces/{workspaceId}/pipelines/{slug}/state/{key}?schedule_id=S
Auth: member plus manage (OWNER/ADMIN). Empty key is 400. The
response is 200 OK — { "slug", "schedule_id", "key", "deleted": true }.
An absent key in the selected bucket is 404; a missing pipeline is 404;
an unavailable state store is 503; delete failure is 500.
Statuses: 200 OK; 400 for an empty key; 401 without authentication;
403 without manage permission; 404 for a missing pipeline or key; 503 for
an unavailable state store; 500 for delete failure.
DELETE /api/v1/workspaces/{workspaceId}/pipelines/{slug}/state?schedule_id=S
Auth: member plus manage (OWNER/ADMIN). The operation clears only the
selected bucket (empty schedule_id selects manual/webhook state). 200 OK
returns { "slug", "schedule_id", "removed": N }. A missing pipeline is
404; an unavailable state store is 503; clear failure is 500.
Statuses: 200 OK; 401 without authentication; 403 without manage
permission; 404 for a missing pipeline; 503 for an unavailable state store;
500 for clear failure.
Single-step execution
POST /api/v1/workspaces/{workspaceId}/pipelines/{slug}/step_run
Auth: member plus create (MANAGER+). This executes one agent_run,
http, script, or transform step against a fixture; it does not traverse
the DAG or create a pipeline run record. The JSON body is:
step_id is required (400); invalid JSON is 400; a missing pipeline or
step is 404; a non-supported step type is 400; a non-active pipeline is
409; an unwired runner is 503; model resolution failure is 500; step
execution failure is 502.
Response: 200 OK with step_id, step_type, output, valid, optional
validation_reason, cost_usd, tokens_in, tokens_out, duration_ms,
simulated: true, and optional warnings. Agent steps additionally include
adapter, model, and rendered_prompt. Unseeded upstream output references
are reported in warnings.
Pipeline tags
PUT /api/v1/workspaces/{workspaceId}/pipelines/{slug}/tags
Auth: member plus create at the route and update in the handler
(MANAGER+). Body is { "tags": ["tag-a", "tag-b"] }; an empty list or
invalid JSON is 400. A missing pipeline is 404; exceeding the tag-store
limit is 400; other lookup/store failures are 500.
Response: 200 OK — { "tags": ["..."] } with the routine’s complete
tag set after the add.
DELETE /api/v1/workspaces/{workspaceId}/pipelines/{slug}/tags/{tag}
Auth: member plus create at the route (MANAGER+). 200 OK returns
{ "ok": true }. A missing pipeline is 404; tag removal failure is 500.
Removing an absent tag is otherwise successful.