Skip to main content
Pipelines are persisted, versioned workflow definitions. All routes below use the /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.

Draft and files

List rows and the single-routine detail carry a draft object while an unpublished revision exists in the drafts store for the slug — absent otherwise, so a client can test for the key. updated_by is the stored user id; there is no lookup.
The detail alone carries files: what the routine runs, always an array ([] when nothing is declared). Sources, in order: every script.path (routine hooks, step hooks and foreach bodies included), then paths under /crew/shared found in script.args / script.env values that name a file.
language is the extension when it is one of go py ts js sh bash yaml yml json md sql, else empty; interpreter is script.interpreter or the one the runner infers from the extension. present, size_bytes, updated_at and description (the file’s first comment lines, at most 200 characters) come from the author crew’s shared volume through the same reader the crew Files panel uses. status says what that check established: present (listed on the share), missing (the share was listed and the path is not on it) or unverified (the share could not be listed — no author crew, container or volume unavailable — or the per-request budget of 25 checked files ran out first). present is the boolean shorthand of status: "present". Only missing means the file has to be put on the share; unverified rows carry no size, time or description, and a file can never fail the detail request.

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. fire_at is a one-time start: an RFC3339 instant (with offset) in the future and within two years, parked in the same pending queue as a delayed run and fired by the dispatcher when the time arrives. It cannot be combined with delay_seconds, debounce_key or ttl_seconds (400). For a one-time start, use a separate request without the delay/debounce fields above. Choose a future instant and an existing published version:
pinned_version runs an archived version instead of head. It must be a positive integer (400 for zero or a negative value); a positive version the routine has not published returns 404. It is accepted only on an immediate or fire_at start; a delayed or debounced start answers 400, because those keep their own version policy. Every deferred start is pinned: when the body names no version, the server pins the version whose hash matches the current head at acceptance and returns it as pinned_version in the 202 receipt, so a publish while the trigger waits does not change what fires. A routine with no archived version is refused with 409 — publish one first. The governance, integration, resource and credential preflight always judges the version that will run, never head.

Run statuses and responses

POST .../pipelines/{slug}/run requires member plus either create (normally MANAGER+) or the routine.run capability, which an admin can grant to a specific member so they can trigger routines without being able to author them. The capability grants the right to ask and nothing more: the governance status gate, the integrations / resources / credentials preconditions and the routine’s spend caps all still apply, and refuse a capability-admitted member exactly as they refuse an admin. run_batch is NOT covered by it and stays MANAGER+. The endpoint 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, a non-empty debounce_key, or fire_at) returns 202 Accepted with { "status": "SCHEDULED", "pending_id", "fire_at", "coalesced", "priority", "pinned_version" } and does not yet have a run ID. Send Prefer: respond-async to get the run ID before the run finishes: the response is 202 Accepted with Preference-Applied: respond-async and { "run_id", "status": "IN_PROGRESS" } as soon as the run row exists, and the run continues on the server’s own lifecycle context rather than the request’s — so a closed connection no longer cancels it. Poll GET .../pipeline-runs/{runId} for the terminal status. If a fast run finishes before the handler observes its start notification, the handler may return the ordinary 200 result instead. A run that fails before it is even registered (a refused preflight, a concurrency conflict) still answers synchronously with the usual status code. Every manual start in the UI uses this header — the detail panel’s Run, a run detail’s Retry, version comparisons, Inbox Retry and slash-command runs. 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.

Run provenance

Rows from GET /api/v1/workspaces/{workspaceId}/pipelines/{slug}/run-records carry the composition fields below, so “why did this run” is answerable without a second request.
triggered_via cannot distinguish an automation from a cron schedule. Every deferred run is fired by the same pending-run dispatcher and stored with triggered_via: "schedule", automations included. Test automation_name for presence to identify a rule-fired run; treat a "schedule" row without it as a cron.

Outcome

Both run-records rows and GET /api/v1/workspaces/{wsId}/pipeline-runs/{runId} carry an outcome field (work package B6, docs/prd/PRD-ISSUES-AND-ROUTINES-2026.md §9.6) — the same deterministic routing decision issue-session runs carry, described in full under Issues → Outcome. In short: NEEDS_HUMAN raises exactly one run_needs_human inbox item with an action contract; a run that ends cleanly but reports no recognised outcome while containing a step that could have reported one is recorded FAILED with error_message set to "no outcome reported"; every other outcome (NO_CHANGE, SUCCEEDED, WORK_CREATED, PARTIAL, CANCELLED) is recorded and otherwise history-only for a routine run today — no comment-posting path exists for pipeline_runs at all, so WORK_CREATED/PARTIAL do not (yet) post to the triggering issue the way §9.6’s table intends; see the note under Issues → Outcome. outcome is parsed from a ---CHECKPOINT--- or ---HANDOFF--- block in the run’s own output when one of the run’s steps is an agent_run step whose prompt asks for one. A routine that HAS such a step (or a call_pipeline, whose target may hold one) and still reports nothing gets the FAILED/“no outcome reported” default. A routine with no agent step at all — an agentless: true probe, or one whose steps are only script / transform / notify / crewship — has nobody who could ever report one, so a clean completion is recorded SUCCEEDED with an empty error_message instead of being flagged for a hand-off it was never able to make. Empty on a non-terminal run, or one that predates the outcome column.

Failure

GET /api/v1/workspaces/{workspaceId}/pipeline-runs/{runId} adds a failure object to a run whose status is failed or interrupted, or whose outcome is FAILED. It is a projection over the raw error_message and failed_at_step, which stay exactly as the engine wrote them; every other run has no failure key at all.

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.

Atomic trigger authoring

trigger is optional and, when present, is created in the SAME transaction as the routine and its version: either all three exist afterward, or none do — a malformed cron expression or unknown timezone rolls the whole save back, not just the trigger. trigger.kind is "schedule" (the fields above; only cron is required, the rest default the same way schedule create does), "once" (a single start at trigger.fire_at, an RFC3339 instant with a timezone offset, in the future and within two years; it is parked as a pending run pinned to the version this save publishes, the response’s trigger is {"kind": "once", "first_fire_at", "enabled": true} with no schedule_id, and a later save of the same routine with a new fire_at moves that one pending start rather than adding another; a start that was cancelled is never re-armed by a save, and one that already fired is only re-armed for a different fire_at — otherwise the save is 422 “this one-time start has already been consumed”), or "manual" (an explicit, on-purpose “no trigger” — distinct from omitting trigger entirely). A "once" trigger needs a routine that is active after this save — one that lands proposed for review is refused with 422. trigger.inputs is the preset the trigger fires with, validated against the definition being saved (422 when it does not fit); on a "schedule" trigger it becomes the plan’s saved inputs, which is how a save that renames a required input carries its plan along instead of being refused with 409 schedule_conflict. Webhook and automation-binding trigger kinds are not yet supported here; create those separately via their own endpoints. activation (top-level, alongside trigger, not inside it) is "draft" to create the trigger DISABLED and raise exactly one escalation inbox item asking a MANAGER+ to turn it on — the response’s trigger.approval_required is true and trigger.first_fire_at names what the first run WOULD be. Omit it to activate the trigger immediately; any other value is 422, and so is "draft" on a "manual" or "once" trigger, which have nothing to hold back. Approve via POST .../pipeline-schedules/{scheduleId}/activate (see Schedules). The response embeds what was created as trigger:
trigger is omitted entirely when the save didn’t request one. A validation failure in trigger (bad cron, unknown timezone, invalid catchup_policy, an unparseable fire_at, or an unsupported kind) is 422 and — per the atomicity guarantee above — the routine is not saved either.

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.

POST /api/v1/workspaces/{workspaceId}/signals

Topic-scoped delivery: the same act as the per-run signal above, addressed by event instead of by run. Use it when the sender knows what happened but not who is waiting — every run in the workspace parked on that event_type wakes, each receiving the payload as its wait step’s output. Auth: member plus update (MANAGER+) — identical to the per-run signal, because delivering one still resumes somebody’s parked run. The body is { "event_type": "mission.status_change", "payload": "..." }; event_type is required and is a free-form string (no registry, no enum). Invalid JSON or a missing event_type is 400. An unwired signal-wait store is 503 — unlike the per-run form there is no in-memory fallback, since the in-process registry is keyed by run id and cannot answer “who is waiting on this event”. Each pending wait is claimed exactly once, so two callers delivering the same topic simultaneously wake disjoint sets of runs and no run is resumed twice. The workspace in the path is the fence: a run in another workspace never wakes. Response: 200 OK{ "ok": true, "delivered": 2, "run_ids": ["run_…", "run_…"] }. Delivering a topic nobody waits on is 200 with delivered: 0, not 404: an event source emits whether or not a routine happens to be listening. A delivery write failure is 500. In the pathological case of more waits than one call can drain, the response adds "truncated": true — the listed runs were delivered and resumed, and calling again claims the rest.

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

monthly_budget_usd is reporting-only — it never stops a run. Setting it does not enforce anything; the executor has no code path that reads it. It only drives over_budget/pct_used on this response and the workspace budget-summary roll-up, so you can see that a routine ran over its planned monthly spend after the fact. The value that actually blocks execution is the DSL’s own max_cost_usd (a per-run hard cap authored into the routine definition and enforced mid-run by the executor) — see the Monthly budget meter section of the routines guide for the full comparison. If you need runs to be refused or aborted for cost, set max_cost_usd, not this field.

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.
See 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, RFC3339Nano fire_at, nullable pinned_version, and the read-only inputs preview ({} when empty). The preview retains safe primitives; credentials, files and known secrets become type-only markers, and other containers are empty. It must not be replayed as run inputs. 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:
When both selectors are supplied, IDs are retained and fingerprint IDs are appended, then the combined list is truncated to 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 uses POST .../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, execution_mode: "live", simulated: true, and optional warnings. execution_mode is the honest label: the step really ran and its side effects landed; simulated is the legacy flag meaning only that no run record was written, not that effects were isolated. 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.