Skip to main content
Crews are isolated agent teams, each with its own container resources, network policy, and members. A crew bundles the runtime sandbox (memory/CPU/TTL, runtime image, devcontainer/mise config), the egress posture (free vs restricted with an allowed-domain list), an autonomy policy, an optional shared persona, and cross-crew connections that gate agent-to-agent messaging. All crew endpoints require authentication and workspace context. The workspace may be supplied as workspace_id (query), {workspaceId} (path), or the X-Workspace-ID header; query/path values take precedence and may be a workspace ID or slug.

Endpoints


Crew CRUD

Create, read, update, and soft-delete crews and their container/network configuration.

List Crews

Returns up to 100 crews in the workspace, ordered by created_at DESC, id DESC. Use limit (maximum 500) and offset for pagination; the onboarding setup crew is never listed and never counted. Use order=name for case-insensitive name order with a stable ID tiebreaker; order=recent (the default) keeps newest first. Paging (every windowed list on this API): the body stays a bare array; the window is described in three response headers so no consumer breaks: q is a case-insensitive substring search on the server (name, slug and purpose/description); it narrows the total as well as the page. The web app and the CLI read the headers to say “showing 1–100 of 103” instead of calling a page the whole. Auth: Session or CLI token + workspace membership Request: No path parameters or request body. Workspace context is required; provide it using the workspace_id query parameter, {workspaceId} path context where supported, or X-Workspace-ID as described above. Response: 200 OK

Response Fields


Create Crew

Auth: Session or CLI token + OWNER, ADMIN, or MANAGER role Request Body:
Two floors. container_memory_mb and container_cpus are rejected with a 400 only below Docker’s own minimums (6 MB, 0.01 CPU) — the daemon refuses to create a container there, so the request could never have worked.Between that and the usable floor (2048 MB, 0.5 CPU by default) the crew is created, and the response carries a warnings array explaining what will break: a warmed agent CLI holds 1.5–2 GiB, so a smaller crew is created successfully and then OOM-killed (exit 137) on start. A small crew can be deliberate, and refusing it would not make it bigger — so this is advice, not a veto. Memory is a cgroup ceiling rather than a reservation, so sizing at the floor costs an idle host nothing.The usable floor is settable per instance via the runtime.agent_min_memory_mb and runtime.agent_min_cpus instance settings. runtime.agent_min_memory_mb is also the divisor behind a crew’s concurrent-run budget.0 on either field means “use the server default”, and it resolves to the same size on POST and on PATCH — the stored row never holds 0.
warnings (array of strings, POST and PATCH responses only) is present only when there is something to say and is otherwise omitted. It is additive — a client that ignores it sees an unchanged crew object.
Response: 201 Created — returns the created crew object (same shape as List response item). Error Responses:

Get Crew

Auth: Session or CLI token + workspace membership Request: crewId is a required path parameter and workspace_id selects the workspace. No request body. Response: 200 OK — full crew object including _count, issue_prefix, mcp_config_json, and escalation_config.

Update Crew

Both PATCH and PUT are supported and behave identically (partial update — only provided fields are changed). Auth: Session or CLI token + OWNER or ADMIN role Request Body: All fields are optional. Response: 200 OK — updated crew object.
Replacing private service settings: there is currently no supported in-place replacement or clearing operation for a configuration withheld on read. Omit services_json to update other crew settings. Creating a replacement crew allows a new service configuration, including runtime credential references (env_refs), but does not migrate the existing crew’s resources. Deleting the old crew also deletes its missions, mission tasks and memberships, soft-deletes its agents, and may remove its auto-managed credentials; review Delete Crew before choosing that path. Safe in-place replacement is tracked in #2843.
Changing network_mode, allowed_domains, or services_json triggers a container restart so the docker provider picks up the new policy / sidecar set on the next agent run. Changing runtime_image, devcontainer_config, or mise_config invalidates cached_image + config_hash to force a rebuild.
container_memory_mb / container_cpus do not restart the container, and do not reach a running one. They are cgroup limits, fixed when the container is created — a running crew keeps the limits it was created with while this response, and every later GET, reports the new ones. The container is deliberately not stopped for them: unlike a stale network policy, a stale memory limit is not a live exposure, and killing whatever the crew is executing costs more than the delay does.When the crew’s container is running, the response carries a warnings entry saying so. A stopped one is rebuilt with the new limits on its next wake and gets no warning, because nothing is pending on the operator. GET /container-status reports the configured and effective figures side by side.

Delete Crew

Soft-deletes the crew (sets deleted_at) and cascade-deletes its missions, mission tasks, and crew members so identifier prefixes can be reused. The slug is rewritten to {slug}_deleted_{id} on next Create reuse.
The crew’s agents are soft-deleted with it, which releases their slugs too: re-applying the manifest that created them succeeds without a manual DELETE /api/v1/agents/{agentId} first. Each agent’s slug is rewritten to {slug}_deleted_{id} when a new agent claims it, exactly as the crew’s is.
The crew’s auto-managed service credentials are soft-deleted with it. A credential is removed only when its row proves the crew owns it (provider=AUTO_MANAGED, created_by_actor_type=system, provisioned_for_service of <crew-slug>/<service> in this workspace) and no live agent, crew or workspace binding outside the crew references it. User-created and shared credentials are never removed. The names removed are returned in removed_credentials.
Auth: Session or CLI token + OWNER or ADMIN role Request: crewId is a required path parameter and workspace_id selects the workspace. No request body. Response: 200 OK
WebSocket event: crew.deleted broadcast to workspace:{workspaceId}

Members

Manage the per-crew membership roster and optional per-crew role overrides that win over the workspace role for crew-scoped permissions.

List Members

Request: crewId is a required path parameter and workspace_id is the workspace query parameter. No request body. Response: 200 OK — array of member objects.

Add Member

Auth: OWNER, ADMIN, or MANAGER role Request Body:
Response: 201 Created — member object with nested user (id/email/full_name/avatar_url). The role override is not echoed on the Add response (it is only included by List Members).

Update Member Role

Elevate or clear the per-crew role override. The crew-member row carries an optional role column that, when set, wins over the workspace-level role for permissions scoped to this crew. Clearing it (empty or omitted role) drops the member back to inheriting their workspace role. Auth: OWNER or ADMIN role on the workspace. A per-crew ADMIN is not allowed to reshape membership — that could ladder a MEMBER straight to OWNER and bypass the workspace gate. Request Body:
Response: 200 OK — a status envelope echoing the applied role (an empty role means the override was cleared):

Remove Member

Auth: OWNER or ADMIN role Request: crewId and memberId are required path parameters; workspace_id is the workspace query parameter. No request body. Response: 200 OK

Persona

The crew layer of the persona stack — PERSONA.md markdown that the orchestrator injects into every agent in the crew, used as the default when an agent has no agent-layer file. Resolution is layered: the agent layer wins if present; otherwise crew default; otherwise the synthesised “You are the …” stub. See the Agent Persona section in the agents reference for the cross-layer model and cap_bytes (1500). All three endpoints are operator-only; there is no crew-flavoured suggest analog (agents propose into their own layer, not the crew default).

Get Crew Persona

Request: crewId is a required path parameter. No request body. Response: 200 OK
Unlike GET /agents/{id}/persona, this endpoint does not report a from_default flag — the crew layer either has a file or it doesn’t. When empty, content is "" and clients fall through to the per-agent synthesised default.

Set Crew Persona

Writes the crew layer file. Records a row in memory_versions keyed to the crew-layer path so the history endpoint can replay the chain. Request:
Response: 200 OK — { "layer": "crew", "bytes": N, "updated": "<rfc3339>" }

Reset Crew Persona

Request: crewId is a required path parameter. No request body. Removes the crew layer file. Subsequent reads return empty content and agents fall back to their per-role synthesised default. Response: 204 No Content

Policy

Each crew carries an autonomy posture that gates every HITL-relevant decision the orchestrator makes — memory writes, skill creation, behavior-monitor escalations, persona suggestions, ephemeral spawns. The dial is read by every PR-B / PR-C subsystem via the shared policy.Resolver. The CLI counterpart is crewship policy get/set.

Get Crew Policy

Response: 200 OK
set_by_user_id / set_at / reason are omitted on crews whose policy still matches the seed defaults (no operator has ever flipped it). On crews that have been touched at least once, the audit triple records who, when, why.

Set Crew Policy

Replaces the policy. Records the audit triple (set_by_user_id, set_at, reason) atomically with the value change and invalidates the resolver cache so the next decision sees the new state. Auth: Session or CLI token + workspace membership (no additional role gate) Request:
Response: 200 OK — same shape as GET, reflecting the new state and the freshly-recorded audit triple.

Avatar

Bulk-apply an avatar style across a crew’s agents, or clear per-agent overrides.

Apply Avatar Style

Applies an avatar_style to all non-deleted agents in the crew, or clears per-agent overrides. This also clears any stored avatar renders on those agents, because a stored render depicts the previous style — leaving it would make this call a no-op for every agent that has one. Affected agents revert to avatar_url: null. The dashboard stores their new renders as part of the same change; after a CLI or API call, store them with the explicit backfill. PATCH /api/v1/crews/{crewId} with a new avatar_style does the same for the narrower set of agents that inherit the crew default (their own avatar_style is null). Agents with an explicit style of their own are unaffected by the crew default, so they keep their stored render. Auth: OWNER or ADMIN role Request Body: Response: 200 OK
When reset_overrides is true, the response is { "updated": N, "reset": true }.

Container

Start Container

Starts the crew’s runtime container and returns once it is running. This is not provisioning. Provision builds the container image; this creates and starts the container itself, running the same sequence an agent run performs — the crew’s provisioned image, its mounts, containerEnv, capabilities and resource limits, and any sidecar services declared in services_json. A crew with no image yet is provisioned first, so the call works on a cold crew (that build can take minutes). Idempotent. Starting a running crew returns its existing container and 200, so a caller can start-then-write without branching on current state. Auth: MANAGER+ (role create) — starting a container spends host memory and CPU, so it sits with provision / rebuild / restart-agents rather than with the reads. Request: crewId is a required path parameter and workspace_id selects the workspace. No request body. Response: 200 OK
Response Fields:
Why this exists. Files under /crew/shared are owned by the container user (uid 1001), so the server can only overwrite one by replaying the write inside the container — which has to be running. Against a stopped crew, PUT /files/save answers 409 file is owned by the crew runtime. Before this endpoint the only way to clear that was to run an agent at the crew with a throwaway prompt, spending tokens for a side effect.notices is returned rather than logged deliberately: the caller asked for this crew to be up, and “up, but without its declared postgres” changes what they do next.
CLI: crewship crew start

Stop Container

Stops the crew’s runtime container and any sidecar services it declared. Proxied to crewshipd, which owns both halves of the operation. Idempotent. Stopping an already-stopped crew answers 200 — that is the state the caller asked for. Auth: MANAGER+ (role create). Not a lower bar than starting: stopping a crew somebody is working in interrupts them, so it is not read-shaped just because it frees memory rather than spending it. Request: crewId is a required path parameter and workspace_id selects the workspace. No request body. Response: 200 OK
No container_id: the container it would name is gone by the time the caller reads the answer, and returning one invites a follow-up call against a dead id.
Named volumes are not removed, so sidecar data survives a stop. Only crewship crew delete removes them (see the crew-delete teardown report).Container memory and CPU limits are applied at container-create time, so stop-then-start is how a resize takes effect on a crew that is already running.
CLI: crewship crew stop

Container Status

Returns the crew’s runtime container state, proxied from crewshipd over the internal IPC socket. Useful for confirming a crew’s container came back up after a network-policy change (which stops the container so it is recreated with the new policy on the next agent run). Auth: any workspace member Request: crewId is a required path parameter and workspace_id selects the workspace. No request body. Response: 200 OK
Response Fields:
Why config_drift exists. runtime_contract asks whether this container is older than the BUILD; config_drift asks whether it is older than this CREW’s own settings. container_memory_mb and container_cpus are cgroup limits, fixed at container-create and unchangeable on a running container, so updating a crew moves the row and this endpoint’s configured_* fields while effective_* stays where it was.Same response as for a stale runtime contract, for the same reasons: a stopped container whose limits no longer match its crew is rebuilt on the next wake, and a running one is reported rather than killed. PATCH /api/v1/crews/{crewId} warns on the spot when it resizes a crew that is currently running.
Why runtime_contract exists. A crew container is created once and reused for as long as it keeps running, and the settings that harden it — the init process that reaps orphans, the core: 0 limit that stops a crashing agent writing its credential environment to disk, supplementary groups, swap, the /dev/shm size — are applied at container-create time and nowhere else. Upgrade the server and a crew that was already running keeps the OLD configuration indefinitely.stale means exactly that: this container predates the current configuration and does not carry what has been added since. Nothing is broken and no run fails; the container picks the new settings up the next time it is recreated — an idle-TTL stop, or crewship crew restart-agents <slug>. A container found stopped with a stale contract is rebuilt automatically on the next wake, because at that moment the rebuild costs nothing. A running one is never torn down for it: that would kill whatever the crew is executing.
Status values are deliberately coarse and the endpoint answers 200 even when the runtime is unavailable, so a polling UI/CLI can treat them as transient states rather than errors: CLI:

Credential Readiness

Reports which of the credentials this crew can use are for a CLI the crew’s container does not have. The sandbox runtime image ships git, curl and jq. Tools like gh, aws, az, gcloud, kubectl, docker, terraform, npm and ansible exist in the container only when the crew’s devcontainer_config declares the matching feature (or mise installs the tool). A valid credential and a working agent are therefore two different things: a healthy GitHub PAT plus a crew with no github-cli feature still gets gh: command not found. The endpoint is read-only and advisory. It never edits devcontainer_config and never triggers a rebuild — adding a feature changes what runs inside the container, so it stays a separate, user-confirmed action (Update Crew or crewship crew config). Request: crewId is a required path parameter and workspace_id selects the workspace. No request body. Credentials are matched using the same three-way grant rule as the rest of the credentials surface: workspace-scoped, crew-scoped via credential_crews, or assigned to one of this crew’s agents (with any lease still live). Revoked and soft-deleted credentials are excluded. A provider with no known CLI (Notion, Stripe, an unrecognised string) reports nothing rather than guessing. Matching is on the tool name, not the feature ref, so an equivalent feature counts: docker-outside-of-docker satisfies a DOCKER credential, and a mise nodejs satisfies an NPM one. Auth: any workspace member Response: 200 OK
Response Fields: CLI:

Crew Capabilities

Returns one authoring-oriented capability bundle for the crew: resolved container resources, enabled crew integrations and their enabled tool names, agent slugs, available code runtimes, script-interpreter mappings, and the routine DSL JSON schema. Auth: authenticated workspace member Request: crewId is a required path parameter. Workspace context is required and may be supplied with X-Workspace-ID (or the equivalent workspace context described at the top of this page). No request body. Response: 200 OK
container.datastores and container.tools are arrays and may be empty. Only enabled crew integrations are included; integrations[].tools contains only explicitly enabled crew-scoped tool bindings. schema is the nested routine DSL schema object, not a JSON-encoded string.

Connections

Crew connections (“crew links” in the UI) define which crews can reach each other. A link is what allows cross-crew task assignment and messages. Shared-file reads and delivery also require the directed file permission; a link alone is not sufficient when file access is disabled. direction is read from from_crew_id’s side: unidirectional means from may hand work to to and not the reverse; bidirectional means either may. Links to a soft-deleted crew are never listed and are removed when the crew is deleted.

Shared-file access

Each connection returns forward_file_access, reverse_file_access and access_version. The forward value applies from from_crew_id to to_crew_id; reverse access also requires a bidirectional active link. Values are none, read, or read_write. read_write allows reading shared files and delivering files through the incoming-file API; it does not grant arbitrary edits to all files, credentials, private agent homes or a shell. Existing links retain their prior read_write behavior. New links created in Settings start with none in both directions. Auth: OWNER, ADMIN or MANAGER in the connection’s workspace. Request: Change one direction with the currently observed version:
Response: 200 OK with {"level":"read","access_version":2}. Statuses: Invalid level/subject/version returns 400, insufficient role 403, a missing or cross-workspace connection 404, and a stale version 409. Reload before retrying 409. Authorization is checked at request admission; an already-open read may finish. Changing file access does not change the communication direction. Each update is audited. POST creation accepts optional forward_file_access and reverse_file_access; omitted values preserve the historical API default read_write. On an existing link POST preserves its file permissions; use the versioned PUT to change them.

List Connections

Request: workspace_id is the required workspace query parameter. No request body. Response: 200 OK — array of connection objects:

Create Connection

Auth: OWNER, ADMIN, or MANAGER role Request Body:
Response: 201 Created for a new link, 200 OK when one already existed.
The call is an upsert on the PAIR, not an insert: a pair is one edge whichever end you name first.
  • Same pair, same orientation → the stored link keeps its id and takes the new direction.
  • Same pair, reverse orientation → the stored link becomes bidirectional (you asked for the way back as well). To make a link one-way in the other direction, delete it and create it again.

Delete Connection

Auth: OWNER, ADMIN, or MANAGER role Request: connectionId is a required path parameter and workspace_id is the workspace query parameter. Optional expected_version is a positive integer from the last list response. With it, deletion is conditional on that version; a concurrent edit returns 409. No request body. Response: 204 No Content

Crew Files (Proxy)

File operations are proxied through the crewshipd sidecar to the crew’s container filesystem.

Delete a Crew File

DELETE /api/v1/crews/{crewId}/files/delete?path={path} Auth: Authenticated workspace member with the create capability. The crew must belong to the selected workspace. Request: No body. The required path query parameter is a relative file path under the crew’s shared/output tree. Absolute paths and traversal are rejected. Response: 200 OK with the sidecar response:
Deletion is idempotent: a missing file still returns success. Statuses: 400 missing or invalid path; 401 unauthenticated; 403 insufficient role/capability; 404 crew not found in the workspace; 500 storage/delete failure; 502 sidecar unavailable. A missing storage provider may return 503 from the sidecar.

Crew Git Diff

Crew Git Diff

GET /api/v1/crews/{crewId}/git-diff?agent_slug={agentSlug} Returns the crew container’s base-branch diff, the same change set used by the dashboard’s Changes tab. agent_slug is optional and selects the agent workspace when the crew has multiple agent workspaces. Auth: Authenticated workspace member with the read capability. The crew must belong to the selected workspace. Request: No body. Optional agent_slug query parameter. Response: 200 OK with the decoded git-diff object from crewshipd. If the workspace is not a repository, the response is { "is_repo": false }. Statuses: 401 unauthenticated; 403 insufficient role/capability; 404 crew not found in the workspace; 500 crew lookup failure; 502 crewshipd unavailable or diff computation failure.

Live Service Inventory

Live Service Inventory

GET /api/v1/crews/{crewId}/services Returns the live sidecar containers, not the stored services_json configuration snapshot. Auth: Authenticated workspace member with the read capability. The crew must belong to the selected workspace. Request: No body or query parameters. Response: 200 OK:
type is one of postgres, redis, mysql, mongodb, or other. A provider without live service-list capability returns 200 with an empty services array. Statuses: 401 unauthenticated; 403 insufficient role/capability; 404 crew not found in the workspace; 500 crew lookup or service-list failure.

Live Container Inventory

Live Container Inventory

GET /api/v1/crews/{crewId}/containers Returns every container the crew has on the runtime right now — its agent runtime container and its sidecars — with live state and usage. Wider than Live Service Inventory, which answers about sidecars only and renames them to their manifest service names; here name is the container’s real runtime name, the one docker logs takes. Auth: Authenticated workspace member with the read capability. The crew must belong to the selected workspace. Request: No body. workspace_id query parameter (or the X-Workspace-ID header) selects the workspace. Response: 200 OK:
kind is crew (the agent runtime) or sidecar (a declared service). status is the live state — running, stopped, creating or error — the same vocabulary Container Status reports. null means unmeasured, never zero. A stopped container has no CPU or memory reading, a runtime without stats support (apple-container) has none for anything, and agent_count is set on the crew row only — no agent runs in a postgres container. Render an absent number as ”—”; rendering it as 0 draws an idle container where nothing was measured. A provider without live container-list capability returns 200 with an empty containers array. The array is always present: a client that treats a missing containers field as “nothing is running” reproduces the defect this endpoint was added to fix (#1697) — treat it as a broken response instead. Statuses: 400 missing crewId; 401 unauthenticated; 403 insufficient role/capability; 404 crew not found in the workspace; 500 crew lookup or container-list failure. A failed per-container stats read is not an error: that row’s cpu_percent and memory_mb come back null.

Crew Assignments

Request: crewId is a required path parameter and workspace_id is the workspace query parameter. No request body. List task assignments for a crew, newest first. limit (default 50, max 100) and offset page through the rows. Response: 200 OK — a bare array of assignment objects:

Crew Peer Conversations

Request: crewId is a required path parameter and workspace_id is the workspace query parameter. No request body. List peer-to-peer conversations between agents in a crew, newest first. Response: 200 OK with a JSON array of conversation objects: id, from_name, from_slug, to_name, to_slug, question, response (nullable), status, duration_ms (nullable), escalated, created_at, finished_at (nullable).

Crew Standup

Request: crewId is a required path parameter; optional query parameters are crew_id and since. No request body. Get standup summary for a crew. Response: 200 OK with crew_id, date, summary, and items fields.

Crew Escalations

An escalation is a question an agent asked a human and blocked on. It has one non-terminal state and three terminal ones: Terminal states are terminal — every transition out of one is a 409 whose message names the state actually reached. The deadline. Each escalation is raised with deadline_at = created_at + 300 s, published to the agent on the create so its wait is bounded by the server’s clock rather than one of its own. When the deadline passes with no answer, the row becomes EXPIRED and the agent continues without the answer, having been explicitly told so — the wait response carries status: EXPIRED and a mandatory warning, and the journal entry is severity warn. The run is not failed and no default answer is invented; see Harbormaster → What happens when nobody answers for the reasoning. Expiry is applied by the long poll the agent is on, by a 60 s background sweeper for rows whose agent is gone, and by the read paths below before they answer. All three go through one compare-and-swap, so a row expires exactly once. deadline_at is null on rows raised before the column existed; those never expire.

List Escalations

Auth: Authenticated workspace member; workspace_id must identify the caller’s workspace. Path / query parameters: crewId is the crew ID; workspace_id is the required workspace ID. Optional status narrows to one lifecycle state (PENDING, RESOLVED, EXPIRED, CANCELLED); an unrecognised value returns an empty list rather than everything, so a typo cannot silently widen the filter. Any past-deadline question in the workspace is settled before the list is built, so a row can never be listed as PENDING past its own deadline_at. Response: 200 OK with an array of escalation objects. Each carries deadline_at (nullable) alongside status, action, resolution, resolved_by and resolved_at.

Resolve Escalation

Request: escalationId is a required path parameter, workspace_id selects the workspace, and the JSON body is the escalation decision (action and, for a TEXT or LINK escalation, the required resolution text). A CREDENTIAL escalation takes no resolution — any text answers 400, a value goes through supply, and approving an ask that still has no value answers 409. Response: 200 OK with the resolved escalation id, status, and action.

Supply Escalation Credential

Auth: MANAGER+ (create role) — the same gate as resolve; supplying the value is the decision, and the credential four-eyes rule applies to it exactly as to approving. Request: escalationId is a required path parameter, workspace_id selects the workspace, and the JSON body is {"value": string, "name"?: string, "type"?: string, "security_level"?: 1..4}. value is required and capped at 64 KiB. name and type are read only for a free-text ask (one that staged no credential); on a structured ask they are ignored and security_level, when set, overrides the tier the agent proposed. This is the only route a human-supplied secret takes (#2376). The value goes into the vault as a handle-only credential; in one transaction the credential becomes ACTIVE, is granted to the asking agent under the name it asked for (leased when the workspace auto-leases), and the escalation is resolved with nothing in its resolution column. The waiting agent is answered with the credential’s name and use: "keeper_execute" — never the value. See Asking a human for a credential. Response: 200 OK with { "id": string, "status": "RESOLVED", "action": "approve", "credential": { "id", "name", "type", "security_level", "handle_only", "granted", "lease_expires_at", "use" }, "agent_still_waiting": boolean, "agent_gave_up_at"?: string, "note"?: string }. The response never echoes the value.

Cancel Escalation

Auth: MANAGER+ (create role) — the same gate as resolve, because closing out a blocking request is the same class of act whichever way it is closed. Request: optional JSON body {"reason": "..."}. The reason is recorded on the row and in the journal entry. Withdraws a question that stopped mattering. Deliberately not the same as resolve --action reject: a rejection is a decision the agent should act on, while a cancellation says nobody ever considered it. Any agent still waiting is unblocked immediately with an explicit “no answer” warning. Response: 200 OK with { "id": string, "status": "CANCELLED" }.

Sweep Expired Escalations

Auth: Authenticated workspace OWNER or ADMIN (manage role) — it writes terminal states. Request: no body. Scoped to the caller’s workspace, so it cannot reach another tenant’s rows. Expires every past-deadline PENDING escalation in the workspace now. The background sweeper and the read paths already do this; the endpoint makes the mechanism operable and observable rather than only inferable from a ticker. Running it twice returns 0 the second time. Response: 200 OK with { "expired": integer }.

Pending Escalation Count

Request: workspace_id is the workspace context. No request body. Returns count of pending escalations across the workspace. Past-deadline questions are settled first, so an unanswered escalation stops being counted once its deadline passes instead of accumulating forever. Response: 200 OK with { "count": integer }.

Purge Crew Escalations

Auth: Authenticated workspace OWNER or ADMIN (manage role). Path / query parameters: crewId is the crew ID; workspace_id is the required workspace ID. The request has no body. The operation permanently deletes all escalation rows for that crew in that workspace. Response: 200 OK

Activity Feed (removed)

The dedicated GET /api/v1/activity aggregator has been removed. It merged three tables (assignments, peer conversations, escalations) in memory and would silently drop a whole source on any query error while still returning 200 OK. The activity feed is now sourced from the journal, which is the canonical event stream:
Filter by crew_id / agent_id, bound the window with since=, and page with cursor / limit (see Journal). Display names and crew colors are not part of the journal row (it stores ids + payload slugs); resolve them via GET /api/v1/journal/lookup. The crewship activity CLI command wraps this same query — see CLI · Activity.

Managed Service State

Declared services can be kept running independently of agent activity. A managed service’s desired state is durable; its last reconciliation is an observation, not a guarantee of current health. The controller retries after runtime failure and reconstructs intent when Crewship starts. Existing services remain on demand until explicitly managed. Manual crew stop persists a stopped intent for its managed services. Starting an agent does not override that stop.

List Service States

Auth: Any authenticated member of the crew’s workspace. Request: Required crewId path parameter and workspace_id query parameter. No body. Response: 200 OK, with supported and services. Each service has name, desired_state, observed_state, and version; managed entries also have a stable id and optional last_error. An unmanaged declaration uses on_demand, unmanaged, and version 0 respectively. No secrets or service environment values are returned. Runtime support is explicit in supported.

Set Service State

Auth: OWNER, ADMIN, or MANAGER in the crew’s workspace. Request: Required crewId, serviceName, and workspace_id. Required JSON body:
desired_state is running (keep running) or stopped; expected_version is the nonnegative version from the last list response. Zero adopts an existing declaration into managed lifecycle. A removed declaration can be stopped using its existing version, but cannot be started. Response: 202 Accepted, for example {"desired_state":"running","version":1,"observed_state":"pending"}. This saves intent; it does not claim the container has started. Poll the list for reconciliation. Missing credentials block startup and leave a visible error; there is no fallback account. Container data volumes are retained through stop/start. Arbitrary agent shell background processes are not managed services.