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 the two audit windows than on run_retention_days,
which rejects it. For an audit table, “keep this forever” is a retention
decision an operator has to be able to express — so null means “use the
product default” and 0 means an explicit never-delete. See
Migrations — 20260810170000.200 OK — updated workspace object.
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)
When the workspace’s license caps the member count, adding a member that would exceed it returns
402 Payment Required with the limit detail in the Problem Details 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/prd/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 eight capabilities:
Presets are named bundles for the common combinations:
A member with no explicit set falls back to a role-derived default (
OWNER/ADMIN → the admin set, MANAGER → chat + routine.create + issue.create + memory.write, others → chat).
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, on top of their own default workspace. 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.
Get pipeline
definition (raw DSL JSON) inlined.
Delete pipeline
OWNER or ADMIN (canRole "delete").
Response: 204 No Content
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.
There is no public
test_run endpoint. You cannot run an agent “dry” (its scripts have uninterceptable side effects), so a real run is just Run. Drafts are validated server-side on /save (parse + schema + cycle detection), and the sidecar agent-authoring flow validates a draft via the internal dry-run gate (/api/v1/internal/pipelines/test_run, X-Internal-Token) before persisting.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:
Response:
200 OK — RunResult.
RunResult shape
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.
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.
List pending waitpoints
200 OK
Approve / reject waitpoint
200 OK
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:61-64).
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