OWNER, ADMIN, MANAGER, MEMBER, VIEWER). Reach for these endpoints whenever you need to read or change a workspace itself or anything scoped under it — its members and their capabilities, invitations, pipelines and their runs/versions/schedules/webhooks, and the workspace skills registry. The standalone workspace-metadata calls (list/create/get/update) sit at the top; everything else is nested under /api/v1/workspaces/{workspaceId}/....
Workspace-scoped endpoints are nested under
/api/v1/workspaces/{workspaceId}/... and require:- A valid JWT session cookie or a CLI token (
crewship_cli_…) in theAuthorizationheader. - The authenticated user to be a member of the workspace (enforced by the
wsCtxmiddleware, which also injectsworkspace_idandroleinto the request context).
type, title, status, detail, and instance.Endpoints
Workspace metadata
Read and manage the workspace record itself — list the ones you belong to, create new ones, and update name/slug/language.List workspaces
created_at DESC. Soft-deleted workspaces (deleted_at IS NOT NULL) are excluded.
Auth: Any authenticated user.
Response: 200 OK
Create workspace
OWNER in a single transaction.
Auth: Any authenticated user.
Request body:
201 Created — same shape as the List response item (without currentUserRole / counts).
Get workspace
200 OK — single workspace object (same fields as List) with currentUserRole populated from the JWT.
Update workspace
preferred_language to an empty string clears it (NULL).
Auth: OWNER or ADMIN (canRole "manage").
Request body: All fields optional.
0 behaves differently on run_retention_days than on the other three:
run_retention_days rejects it, while credential_audit_retention_days,
audit_log_retention_days and approvals_retention_days all accept it as an
explicit “keep forever”. For those three, null means “use the product
default” and 0 means never-delete — a distinction that matters because
they share one command (workspace update), so 0 has to mean the same
thing on all of them or an operator moving between flags gets a silent
surprise. approvals_queue earns the same sentinel as the audit pair
despite not being a compliance table: AfterDecide’s journal entry captures
only approval_id/kind/comment, not reason or the full payload, so
approvals_queue is the only place those survive at all, and an operator
wanting to keep them indefinitely needs a real way to say so. See
Migrations — 20260810170000 and
Migrations — 20260901134904.200 OK — updated workspace object.
Workspace logo
ADMIN+ and writes an audit entry
(workspace.logo.update / workspace.logo.remove); both answer the workspace
object, whose logo_url points at the GET route with a ?v= cache buster
(or is null after a removal). The GET route needs a signed-in user but not
membership, because instance administrators see every workspace.
CLI:
crewship workspace logo set <file> / crewship workspace logo remove.
Delete workspace
deleted_at is set; membership join rows are removed). Deleted crews stop
serving dispatches immediately; their runtime containers are reaped by the
orchestrator’s idle-TTL lifecycle, the same path a single crew deletion
relies on.
On success the server broadcasts realtime events on the workspace channel —
a crew.deleted per removed crew and a final workspace.deleted
({ id, slug }) — so connected clients redirect/refresh without a manual
reload.
Auth: OWNER only. The route sits behind canRole "manage" (ADMIN+)
at the router, and the handler additionally requires the caller to be the
workspace OWNER.
Request body:
Response:
200 OK — { "success": true }.
Drive it from the CLI with
crewship workspace delete.
Members
List the people in a workspace, add an existing user directly, provision a member by email, or remove one.List members
created_at ASC, joined to users for display fields.
Response: 200 OK
Add member
OWNER or ADMIN (canRole "manage").
Request body:
201 Created — the new member row.
License limit (402 Payment Required)
License limit (402 Payment Required)
Current releases allow unlimited members and do not return this license error. Resource licensing may change in future releases. If future enforcement caps the member count, adding a member that would exceed it returns
402 Payment Required with the limit detail in the JSON error body. The check runs before the request body is read, so a capped workspace rejects every add regardless of payload. The same limit applies to Create invitation.Provision member
OWNER or ADMIN (canRole "manage").
Request body: email (required), role (optional; defaults to MEMBER;
one of ADMIN, MANAGER, MEMBER, VIEWER), and full_name (optional).
Response: 201 Created
setup_url is an empty string for an existing account. A malformed email or
role returns 400; a caller below ADMIN returns 403; an existing
membership returns 409; and a missing public instance URL returns 503.
Remove member
OWNER or ADMIN (canRole "manage").
Response: 200 OK
- Pages the member owns in this workspace are transferred to a crew
first, per
docs/specs/pages.md§7.1 rule 1b: the crew owning the most panels on the page, else a crew the departing member belongs to. If any page resolves to neither, the whole removal is refused with409and nothing changes — reassign those pages by hand and retry. - Every crew membership the user holds in this workspace is deleted,
in the same transaction as the
workspace_membersrow: both or neither. Crew membership grants on its own (crew-owned pages, crew credentials, and a per-crew role override folded into the effective role), so it cannot outlive the workspace membership it was predicated on.
crews.workspace_id.
Change member role
MANAGER+ (canRole "create"),
then subject to the role ladder enforced in the handler.
Auth: MANAGER or above, plus the ladder:
- You may only grant a role strictly below your own (
roleRank[new] >= roleRank[caller]is rejected). AnOWNERcan therefore grant up toADMIN, anADMINup toMANAGER, and so on. This means ownership is not transferable through this endpoint. - You may not modify a member ranked above your own.
- The last
OWNERcannot be demoted.
Response:
200 OK
Drive it from the CLI with
crewship workspace member role.
Member capabilities
Capabilities are per-member string grants layered on top of a member’s RBAC role. They let a workspace admin hand an individual user a specific higher-tier action — “let thisMEMBER create routines” — without promoting them to MANAGER. The role still sets the baseline; capabilities only ever widen it.
The closed set of ten capabilities (allCapabilities in internal/api/capabilities.go):
Presets are named bundles for the common combinations:
A member with no explicit set falls back to a role-derived default (
OWNER/ADMIN → the v109 admin set (no routine.run) plus credentials:reveal, MANAGER → chat + routine.create + issue.create + memory.write, others → chat). An explicit set is always authoritative, so revoking credentials:reveal from an ADMIN sticks.
That fallback is not the preset of the same name. It reproduces what the
v109 migration wrote into the capability column, so a row the migration filled
and a row still NULL resolve identically — and that migration ran before
routine.run existed, so the fallback does not include it while the power
and admin presets do. Nothing rests on the difference: every role at
MANAGER+ clears the run gate on role alone. The capability lists returned by these endpoints are always sorted alphabetically.
All three endpoints require the caller to be ADMIN or OWNER.
Path-param note: on the per-member capability endpoints the
{memberId} segment is the member’s user ID (it is matched against
workspace_members.user_id), not the workspace_members.id row ID used
by Remove member. The user_id field in the response
echoes the value you passed in.Get member capabilities
ADMIN or OWNER.
Response: 200 OK
Update member capabilities
Empty arrays are rejected (a
400, not treated as a no-op) — to reset a member to chat-only, send {"set": ["chat"]} explicitly. chat is always implied: it is silently kept on set, and revoking it is rejected.
ADMIN or OWNER.
Guards:
- A caller cannot mutate their own capability row (defence against a downgrade-then-restore stunt) —
403. OWNERcapability rows are immutable — any attempt returns403.- The request body is capped at 16 KB — a larger body returns
413.
200 OK — the post-mutation state (same shape as Get):
List all member capabilities (bulk)
created_at ASC so the roster renders stably between page loads.
Auth: ADMIN or OWNER.
Response: 200 OK
Invitations
Invitations issue a token-gated link that the recipient redeems via the auth flow. They expire after 7 days.List invitations
created_at DESC, joined to users for the inviter.
Response: 200 OK
Create invitation
OWNER or ADMIN (canRole "manage"). Assigning the ADMIN role requires the caller to be OWNER.
Request body:
Response:
201 Created — invitation object with the freshly minted hex token (this is the only time the token is returned).
Invitations are redeemed at signup: when the invited address creates
an account via POST /api/v1/auth/signup,
every live invitation for that address is consumed inside the same
transaction and the new user lands in each inviting workspace at the
invited role. An invited person gets no default workspace of their own;
only a signup with nothing to redeem (no live invitation to a workspace
that still exists) gets one. An invitation for
an address that already has an account is left pending — signup is a
no-op there, and redeeming it would place someone who never consented.
This is also why POST .../members takes a user_id and not an email:
resolving an arbitrary address would be an account-existence oracle.
Pipelines
Pipelines (also called routines in the UI) are versioned, workspace-scoped DSL programs that orchestrate agent runs, sub-pipeline calls, parallel forks, and approval waitpoints. See the Pipelines guide and the DSL reference for authoring details.List pipelines
author_agent_name (best-effort lookup) and the most recent 3 issue identifiers bound via missions.routine_id so the UI can render an “ENG-5, ENG-9 +1” chip without a second fetch.
Query parameters:
Response:
200 OK
definition field is omitted from the List response and only returned by Get pipeline.
step_count is the number of steps in the stored definition (omitted when the
definition no longer parses). head_version is the published version the
routine currently runs. last_recorded_run_id, last_run_outcome,
last_invoked_at and last_invocation_status (lowercase: completed,
failed, waiting, …) come from the most recent pipeline_runs row on
both List and Get, and are omitted while the routine has never run. status is the
governance state (active, proposed or disabled); icon and color are
the presentation choices and are omitted until one is chosen.
Get pipeline
definition (raw DSL JSON) inlined.
Delete pipeline
OWNER or ADMIN (canRole "delete").
Response: 204 No Content
List routine drafts
MANAGER+.
Response: 200 OK, an array of up to 100 {slug, revision, updated_at}
entries, newest edit first. Draft documents are read separately. 403 means
insufficient role; 500 means the draft list could not be read.
Read a routine draft
MANAGER+.
Response: 200 OK, a RoutineDraft envelope containing id, workspace_id,
slug, revision, base_pipeline_id, base_revision, document, updated_by,
created_at and updated_at. With no saved draft, revision zero describes the
current published recipe and its base revision. With no published recipe either,
the document is {} and the base ID is empty. Missing drafts are therefore not
404. 403 means insufficient role; 500 means the read failed.
Save a routine draft
MANAGER+; the authenticated user supplies updated_by.
Request body: the draft envelope returned by the read operation, with edits
inside document. Preserve id, revision, base_pipeline_id and
base_revision. The document contains the ordinary save fields, including
slug, name, description and an object definition; its slug must match the
envelope. Incomplete definitions can be saved. Temporary publication tokens and
legacy validation/governance bypass fields are discarded.
Response: 200 OK, the exact newly stored RoutineDraft with its incremented
revision. An unpublished draft can move to a free slug without changing its ID;
a draft editing a published recipe cannot. Saving does not publish or activate
a trigger. 400 means malformed input; 403 means insufficient role; 409
means the revision/base changed or the target slug is occupied; 500 means the
write failed. Keep the returned envelope for the next edit or publication.
Discard a routine draft
MANAGER+.
Request body: {"id":"draft-id","revision":2}. Both identify the exact
saved draft; a discarded and recreated draft has a different ID.
Response: 204 No Content. Only that draft revision is deleted. 400 means
invalid JSON; 403 means insufficient role; 409 means the ID/revision no longer
matches; 500 means deletion failed. The published recipe remains available.
Publish a routine draft
MANAGER+; normal publication capability review still applies.
Request body: {"id":"draft-id","revision":2,"save_token":"...","approve_risk":false}.
First obtain save_token from POST /api/v1/workspaces/{workspaceId}/pipelines/test_run
for the exact saved definition and author crew. approve_risk: true explicitly
confirms capability changes when publication reports that review is needed.
Response: 201 Created, the saved recipe (RoutinePublishResponse) with its
version and optional trigger result. Publishing atomically verifies and consumes
the exact draft, checks its published base, and saves the recipe and trigger.
Enabled recurring schedules that follow the head must have compatible inputs;
disabled and version-pinned schedules do not block publication.
400 means malformed input; 403 means insufficient role; 409 means stale
revision/base, capability review required, or incompatible schedule inputs;
422 means an invalid definition, trigger or publication proof; 500 means
storage failed. Conflicts preserve both the draft and published recipe.
A schedule conflict includes schedule_conflict: {schedule_id, name, reason};
a capability-review conflict includes risk_reasons.
Save pipeline
authored_via is always set to "user_api" and author_user_id is extracted from the JWT (callers cannot forge identity).
Auth: MANAGER+ (canRole "create").
Request body:
Response:
201 Created — full pipeline object including definition, plus
trigger when the body requested one.
A
409 from the schedule-preset gate carries the plan that blocks the save so
a client can open it rather than parse prose:
error is always publication blocked by schedule <name>: <reason>. reason
is Input <name> is required by the new recipe for a missing required input,
input "<name>": <what is wrong> for a value of the wrong type or outside its
choices, or Stored inputs are unreadable.
Two kinds of enabled plan are judged: plans that target this routine and
follow head (no version pin), against their inputs; and plans that use this
routine as their wake probe, pinned or not, against their wake_inputs.
Disabled plans, and pinned plans targeting the routine, never block a save. A
re-save whose definition is byte-identical to the stored one is never judged
either, so a rename or description edit cannot start failing over a preset
that was already imperfect. The same envelope comes back from
Import, Rollback and the
sidecar’s save_routine door. The definition is left unchanged on 409.
Test an unpublished definition
MANAGER+.
Request body: definition (object, required), author_crew_id (optional
string belonging to this workspace), and sample_inputs (optional object).
The endpoint checks the definition and applicable resource gates, then produces
a static execution plan. It does not execute agents, scripts or HTTP steps.
Response: 200 OK, a RunResult. A successful DRY_RUN_OK result includes
save_token when the server’s signing key is configured. Send that token to
/publish or /save within five minutes, with the same definition bytes. The
token is bound to this workspace, authenticated user and definition hash; a
successful response without a token is insufficient for normal publication.
400 means malformed input, 403 includes foreign crew references, 422
means an invalid definition, and 503 means the runner is not wired. Resource
gates can also refuse the test with their specific missing-resource response.
The separate sidecar endpoint /api/v1/internal/pipelines/test_run authenticates
with X-Internal-Token. For actual external work, use Run.
Dry-run a saved pipeline
manifest — no agent invocations, no journal entries. The dry-run is an honest static plan, not a proof the run will succeed.
Request body: Same shape as Run. All fields optional.
Response: 200 OK — a RunResult (with would_execute) plus a sibling manifest object:
manifest is the routine’s full declared blast radius. It is best-effort: a stored definition that no longer parses leaves manifest null and still returns the report.
Run pipeline
RunResult; for live progress, subscribe to the workspace WebSocket channel and filter pipeline.* journal entries by run_id.
Request body:
Headers:
Runs started on this route are recorded as user-driven:
invoking_user_id is the authenticated caller and invoking_crew_id / invoking_agent_id are empty. The route does not read any X-Crewship-Invoking-* header — a JWT or CLI-token caller has no verified crew or agent identity, and a header would only let a member attribute the run (and the “From” crew on any approval card it raises) to a crew of their choosing. Agent-invoked runs are attributed through the sidecar’s run_routine tool, whose identity fields are checked server-side against the internal token’s crew binding and the agents table.
Response: 200 OK — RunResult.
RunResult shape
RunResult comes back 200 with status: "FAILED",
failed_at_step and error_message. In particular, an HTTP step whose
credential_ref cannot be resolved — the credential is missing, empty, or the
credential store errors — fails that step before the request is sent; the
step error names only the declared credential type
(internal/pipeline/runner_http.go).
List pipeline runs (journal-backed)
pipeline.run.* journal entries for the named pipeline, newest first.
Query parameters:
Each row carries
id, ts, entry_type, severity, summary, pipeline_id, run_id, and the raw payload.
List pipeline run records (projection table)
pipeline_runs (migration v83). Faster than /runs because it skips the journal LIKE-pattern + json_extract path. Returns 503 with a legacy: "/runs" hint when the run store isn’t wired.
Query parameters:
Response:
200 OK — array of run records. Each record:
error_message is sanitised in this view (single-line, ≤200 chars). The full error stays in journal_entries.
List active runs (in-memory registry)
RunRegistry. Used by the dashboard’s “running now” badge and cancel buttons. Single-instance scope — in a multi-replica deployment, each replica only sees its own runs until a shared registry lands. Returns an empty list when the registry is not wired.
Response: 200 OK
List workspace runs (cross-pipeline feed)
/activity page. Returns recent runs across every pipeline with enrichment (pipeline_name, issue_identifier when triggered_via=issue). Sorted by started_at DESC.
step_outputs is deliberately not included in this feed — it can carry many KB of agent transcript/tool-result JSON per row, and this endpoint is polled every few seconds by the dashboard. Fetch Get pipeline run for a specific run id to get its step outputs.
Query parameters:
Response:
200 OK
Get pipeline run
pipeline_runs, joined to pipelines and missions for human-readable enrichment. step_outputs_json is parsed server-side into an object so the UI does not have to JSON.parse twice.
Response: 200 OK — single row. Includes id, workspace_id, pipeline_id, pipeline_slug, pipeline_name, status, mode, current_step_id, step_outputs (parsed), output, started_at, ended_at, error_message, failed_at_step, cost_usd, duration_ms, triggered_via, triggered_by_id, idempotency_key, inputs (parsed), and issue_identifier. Unlike the workspace-runs feed, this view does not carry the invoking_crew_id / invoking_agent_id / invoking_user_id attribution fields.
Run-window file changes
invoking_crew_id, falling back to the pipeline’s author_crew_id), the crew’s host files are listed over the same IPC path the agent/crew file endpoints use (agent /output + /crew/shared), and only files whose modification time falls inside the run’s [started_at, ended_at] window are returned. A still-running run uses “now” as the upper bound.
Auth: read capability (any non-empty role). The resolved crew is re-validated against the workspace before its files are read (tenant guard).
Response: 200 OK
{ "crew_id": "", "files": [] }. Download a listed file via crew files download / crewship crew files get <crew_id> <path>.
This is a correlation, not proof of authorship, and deliberately does not modify the pipeline executor. Because it keys on modification time within the window, the list can also include files written by a concurrent chat session on the same crew, files from another run whose window overlapped, or files authored earlier and merely re-saved (an mtime bump with no content change). Read it as “what changed on the crew while this ran”. See the
routine result CLI docs.Cancel pipeline run
ctx.Err() between steps and propagates cancellation into the agent runner, which kills the underlying CLI process.
Idempotent: cancelling an already-cancelled run is a no-op (200 with the same response). Cancelling a finished run returns 404 because the in-memory registry only tracks live runs.
A run parked on an approval or event (WAITING) is cancelled in the run store too: the row becomes CANCELLED, its pending waitpoints are cancelled, and the response carries "parked": true. The cancel and any resume of that run (approve, signal, event, restart) exclude each other, so exactly one of them wins. If resumes keep taking the run, the endpoint answers 409 without changing anything; retrying the cancel is safe.
Auth: OWNER or ADMIN (canRole "manage") — cancelling another user’s run is a manage-tier action.
Response: 200 OK
Versions
Every save snapshots the pipeline, so you can inspect its history, fetch a specific revision, or roll the head pointer back.List versions
limit (default 100).
Response: 200 OK
Get one version
definition.
Rollback to a version
definition_json back to the named version. History is preserved — rollback does not delete newer versions.
Auth: OWNER or ADMIN (canRole "manage").
Request body:
200 OK — full pipeline object with the restored definition.
Import / export
Move a pipeline between workspaces as a portable bundle — export from one, import into another.Export pipeline bundle
crewship-pipeline-bundle/v1 document. Author identity, runtime stats, and any installation-specific data are deliberately stripped — the receiving workspace fills them in at import time.
Query parameters:
Response:
200 OK
The bundle’s
metadata.head_version field is misnamed in the current build — it actually carries invocation_count. Treat it as opaque metadata.Import pipeline bundle
imported_from_url for audit.
Imports skip the save validation gate by design — a marketplace bundle is presumed to have been validated in its source workspace.
Auth: MANAGER+ (canRole "create") — importing creates a new pipeline row, same privilege as Save.
Request body: the bundle JSON plus an explicit author_crew_id:
201 Created — full pipeline object.
Approval waitpoints
When a pipeline hits astep_wait of kind approval, the run parks and a waitpoint row is created. The UI’s /inbox lists pending waitpoints; approving or rejecting wakes the parked run.
Typed human decisions
An approval wait can declaredecision_form with fields (routine input
specifications) and actions (id, label, approved). The server freezes this
form when it creates the waitpoint. Later recipe edits cannot change the question
or validation for a pending decision. Standing trust does not auto-answer a form.
The authenticated approve endpoint accepts the flat request
{"approved":true,"action_id":"go","data":{"count":0,"enabled":false}}.
The action must exist and its approved value must match. The server validates
required fields and types without converting strings into numbers or booleans.
Rejection relaxes required fields. 400 means an invalid answer, 403 means an
unauthorized actor, and 409 means the decision was already answered or expired.
The accepted JSON answer is the wait step output for downstream steps.
Typed decisions require an authenticated workspace member with MANAGER+;
the public token callback returns 403 even for a well-formed form answer.
Their pending-list rows carry decision_form and omit callback_url.
Legacy binary approvals retain the public callback and their historical output.
Identity comes from authentication, never from an answer’s data fields.
List pending waitpoints
200 OK
Approve / reject waitpoint
crewship routine waitpoints approve) and from the holder of the token itself on the public callback. It refuses an agent — a crew-bound X-Internal-Token, a sidecar tool, a peer agent posting “GO” — and a request with no principal at all, with 403 and "reason": "waitpoint_decider_not_allowed". The waitpoint is left pending and the attempt is recorded in the audit log under the action waitpoint.decision_refused (crewship audit --action waitpoint.decision_refused), naming the actor and the run. No agent-facing route reaches this door today (internal tokens get 401 from the authed route before it), but the door checks the actor’s kind regardless, so wiring one later cannot become a way for an agent to satisfy a waitpoint. The inbox card carries the same audience as who_can_act: ["role:MANAGER"].
Request body:
An approved legacy wait returns the literal
waited:approval:approved as its
step output. An approved typed wait returns serialized DecisionAnswer JSON
with action_id, data, and optional comment; downstream steps can parse
that JSON. Rejected decisions follow the wait step’s failure behavior.
Response: 200 OK
Defaults differ from the public callback — intentionally
This authed endpoint and the public token callback below both accept an optionalapproved field, but they resolve a missing field in opposite directions:
- Here (authed,
.../approve): omitted →false(deny). This is the JWT-authenticated, human/MANAGER+-driven path (requireRole(... "update")). A caller with a valid session making a considered decision is expected to state it; if a request is malformed enough to drop the field, failing closed means the gated run stays parked rather than being silently released. - Public token callback (
POST /api/v1/waitpoint-tokens/{token}): omitted →true(approve). This endpoint has no JWT — the token in the path is the only credential — and exists for external systems (CI jobs, vendor webhooks) whose completion signal is often just “the task finished, hit this URL” with no body at all (trigger.devwait.forTokenparity). Requiring a JSON body here would break that integration pattern.
approved, and do not rely on either default when authoring a new integration: send the field explicitly.
Complete a waitpoint by token (public callback)
trigger.dev wait.forToken parity). An external system holding a waitpoint token completes the wait via an HTTP callback — no workspace JWT required: the high-entropy token in the path is the auth surface, the same model as the public webhook dispatch endpoint. This lets a human-in-the-loop or external-task wait be resolved by a third party (approval service, CI job, vendor webhook) rather than only through the inbox. The token and its callback_url are surfaced on the pending-waitpoints listing (internal/api/router_pipelines.go:127-131).
Auth: none — the path token is the credential. The completion is attributed to the sentinel decider external-callback (not a user id) so audit queries can distinguish callback completions from inbox approvals (internal/api/pipeline_waitpoint_callback.go).
This is not an agent door. The token travels only to person-facing surfaces — the inbox card, the pending-waitpoints listing, the CLI — and is never handed to an agent, so the only way an agent could reach this route is a person choosing to give the token away. The resolve door behind it is the same one the authed endpoint uses and refuses an agent actor by kind.
Request body: optional. A bare POST with no body means “the external task finished, continue the run”.
200 OK
internal/api/pipeline_waitpoint_callback.go:74-84).
Schedules
Schedules fire pipelines on a cron expression. The in-process scheduler ticks every minute and skips soft-deleted rows.List schedules
200 OK
Create schedule
target_pipeline_slug (UI-friendly) or target_pipeline_id (CLI-friendly).
Auth: MANAGER+ (canRole "create"), or a MEMBER holding the routine.create capability.
Request body:
Response:
201 Created — schedule object.
Update schedule
target_pipeline_version: an absent field keeps the existing version pin, an explicit "target_pipeline_version": null clears it (fires track head again). Likewise wake_fail_closed: absent keeps the existing policy, an explicit boolean sets it; clearing the wake gate resets the policy to fail-open.
Auth: OWNER or ADMIN (canRole "manage").
Response: 200 OK — updated schedule.
Delete schedule
OWNER or ADMIN (canRole "delete").
Response: 204 No Content
Webhooks
Webhooks let external systems trigger a pipeline by POSTing to a public URL. Each webhook has its own opaque token and optional HMAC signing secret. The public dispatch entrypoint (POST /api/v1/webhooks/{token}) is not under /workspaces/... — see the Webhooks API doc.
List webhooks
signing_secret value is never returned outside the create response.
Response: 200 OK
Create webhook
MANAGER+ (canRole "create") — a webhook mints a public dispatch URL, so creation is gated at MANAGER+.
Request body:
Response:
201 Created — webhook object including signing_secret (this is the only time it is returned, whether supplied or auto-generated; subsequent reads return only signing_secret_set: true).
Delete webhook
OWNER or ADMIN (canRole "delete").
Response: 204 No Content
Skills
These endpoints manage the workspace-scoped write surface for the skills registry. Browsing skills (GET /api/v1/skills) is not workspace-scoped and lives in the Skills API doc.
Import skill (URL or paste)
SKILL.md either by URL (SSRF-validated, HTTPS-only, no loopback / private addresses) or by pasted content. Re-imports of the same slug update in place.
Auth: MANAGER+ (canRole "create").
Request body:
Response:
201 Created
created is true for a fresh row, false for an in-place update of an existing slug). Scan status and display name are persisted on the skill row but not echoed here — fetch the skill via GET /api/v1/skills/{skillId} to read them.
Bulk import from git repo
SKILL.md files and upserts each through the same license-gated path as single-import. Per-skill rejections are reported in the response’s skipped list rather than failing the whole batch.
Local-path bulk import (the importer’s
Paths field beyond the optional in-repo subdirectory filter) is intentionally not exposed — it would turn the endpoint into an arbitrary host-FS read primitive.MANAGER+ (canRole "create").
Request body:
Response:
200 OK
Generate skill (LLM)
source='GENERATED'. The generated content can then be edited via the regular import flow.
Requires an ACTIVE Anthropic credential of type API_KEY (a real sk-ant-... — OAuth bearers from the Claude Code login flow are rejected because they only work against claude.ai, not the Messages API).
Auth: MANAGER+ (canRole "create").
Request body:
Response:
201 Created
Delete skill
OWNER or ADMIN (canRole "manage"). The skills registry is shared global state; destructive operations get the higher tier.
Response: 200 OK
preferred_language values
preferred_language accepts either the canonical English name or an ISO code. The resolver normalises both to the canonical name before storage. Supported values:
Afrikaans (af), Arabic (ar), Bulgarian (bg), Bengali (bn), Catalan (ca), Czech (cs), Danish (da), German (de), Greek (el), English (en), Spanish (es), Estonian (et), Persian (fa), Finnish (fi), French (fr), Hebrew (he), Hindi (hi), Croatian (hr), Hungarian (hu), Indonesian (id), Italian (it), Japanese (ja), Korean (ko), Lithuanian (lt), Latvian (lv), Malay (ms), Norwegian (nb), Dutch (nl), Polish (pl), Portuguese (pt), Portuguese (Brazil) (pt-BR), Romanian (ro), Russian (ru), Slovak (sk), Slovenian (sl), Serbian (sr), Swedish (sv), Swahili (sw), Tamil (ta), Thai (th), Turkish (tr), Ukrainian (uk), Urdu (ur), Vietnamese (vi), Chinese (zh), Chinese (Traditional) (zh-TW).
The list lives in internal/api/workspaces.go and must stay in sync with lib/languages.ts on the frontend.
See also
- Crews API — crews live inside a workspace
- Skills API — read-side / cross-workspace browse endpoints
- Webhooks API — public dispatch entrypoint for the tokens minted here
- Orchestration guide — how the orchestrator binds crews, agents, and pipelines
- Routines guide — authoring DSL pipelines end-to-end
- Routines cookbook — copy-paste pipeline examples
- Credentials guide — required for the
/skills/generateendpoint
Test a routine step with sample data
MANAGER+.
Request body: definition (object) and step_id (string) are required.
inputs is an optional object of recipe input values; step_outputs maps
upstream step IDs to output strings. fixture_output supplies a replacement
output for an agent, HTTP or script step. An empty string is an explicit sample.
Transforms compute with the production transform implementation using the
supplied inputs and step outputs. External steps only check the supplied output;
the endpoint never calls agents, services or scripts. Only top-level steps are
supported. Dependencies, conditions, retries and outcome graders are not run;
external schema references are blocked. Optional env and secrets objects map names to sample strings; metadata
supplies sample values for run.metadata.*. run.* aliases use the env map,
as in live rendering. These are explicit test data, never values read from the
server environment or credential vault. Use fake values for secret samples.
Missing references cannot silently become empty values. Runtime samples are
included in the fixture evidence hash.
Response: 200 OK, RoutineFixtureResult: execution_mode, step_id,
step_type, definition_hash, fixture_hash, output_source, output, valid,
validation_declared, optional validation_reason, and limitations.
valid: false reports a failed output check; no declared check is distinguished
by validation_declared: false. These results are not publication proofs.
400 means invalid definition, unsupported step type, missing sample values or
malformed input; 403 means insufficient role. A failed output check returns
200 with valid: false so clients can display its evidence.
Get member resource access
workspaceId is the canonical workspace ID; memberId is the membership row
ID, not the user ID.
Auth: Authenticated current trusted OWNER/ADMIN of this workspace. Scoped
CLI tokens additionally require workspace:admin or its wildcard. Existing
restricted-session restrictions also apply to mixed memberships.
Request: No body. Path parameters: workspaceId, memberId. No additional
query parameters are required.
Response: 200 OK, Cache-Control: no-store:
rights is an explicit array, including when empty. Agent operations are
discover, chat, run, delegate; project operations are list, read,
write, delete. Grants do not open application routes whose restricted
runtime integration is incomplete.
Replace member resource access
workspace:admin or its wildcard. The server derives
the operator from authentication, never the request body.
Request: JSON body, maximum 64 KiB. All four fields are required:
membership_id must match the path’s membership row. revision must match the
current version read from GET. mode is trusted or restricted; rights
contains at most 256 distinct {kind,id,operation} objects belonging to this
workspace. Null/omitted rights and unknown fields are rejected. Empty restricted
rights revoke all resource grants. Trusted mode requires empty rights and
restores ordinary workspace access. OWNER/ADMIN targets cannot be restricted.
Response: 200 OK, Cache-Control: no-store, the same complete policy shape
as GET with the new revision. Revisions are opaque versions and may advance by
more than one. No live shared process is forcibly terminated by this endpoint;
new authorization checks enforce the updated policy. Unintegrated restricted
execution routes remain denied.