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_id query parameter).
Endpoints
Crew CRUD
Create, read, update, and soft-delete crews and their container/network configuration.
List Crews
GET /api/v1/crews?workspace_id={workspaceId}
Returns all crews in the workspace, ordered by creation date descending.
Auth: Session or CLI token + workspace membership
Response: 200 OK
[
{
"id": "cm1abc123",
"workspace_id": "ws_123",
"name": "Engineering",
"slug": "engineering",
"description": "Core engineering team",
"color": "blue",
"icon": "code",
"avatar_style": "bottts",
"container_memory_mb": 4096,
"container_cpus": 2.0,
"container_ttl_hours": 24,
"network_mode": "restricted",
"allowed_domains": ["api.github.com", "registry.npmjs.org"],
"mcp_config_json": null,
"escalation_config": null,
"issue_prefix": "ENG",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"_count": {
"agents": 3,
"members": 2
}
}
]
Response Fields
| Field | Type | Description |
|---|
id | string | Crew ID |
workspace_id | string | Parent workspace ID |
name | string | Display name |
slug | string | URL-safe identifier |
description | string? | Crew description |
color | string? | Palette color ID |
icon | string? | Lucide icon name |
avatar_style | string? | Avatar style for agents (e.g., "bottts") |
container_memory_mb | integer | Container memory limit in MB |
container_cpus | number | Container CPU limit |
container_ttl_hours | integer? | Container TTL in hours |
network_mode | string | "free" or "restricted" |
allowed_domains | string[] | Allowed domains (restricted mode only) |
mcp_config_json | string? | Raw MCP server config JSON |
escalation_config | string? | Escalation policy config JSON |
runtime_image | string? | Custom runtime image (e.g., ghcr.io/org/runtime:1.0) |
devcontainer_config | string? | Raw devcontainer.json (max 100 KB) |
mise_config | string? | Raw mise config TOML (max 10 KB) |
services_json | string? | Sidecar services JSON (max 64 KB) |
cached_image | string? | Locally built image tag (set by provisioner) |
config_hash | string? | Hash of devcontainer/mise/runtime inputs used for cached_image |
max_ephemeral_agents | integer | Per-crew quota enforced by the hire flow; always present on List/Get/Update responses |
issue_prefix | string? | Issue identifier prefix (e.g., "ENG"); always null on List (only populated by Get) |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
_count.agents | integer | Number of active agents |
_count.members | integer | Number of crew members |
Create Crew
POST /api/v1/crews?workspace_id={workspaceId}
Auth: Session or CLI token + OWNER, ADMIN, or MANAGER role
Request Body:
| Field | Type | Required | Default | Description |
|---|
name | string | Yes | — | Display name (2-100 characters) |
slug | string | Yes | — | URL-safe identifier (2-50 chars, ^[a-z0-9][a-z0-9_-]*$) |
description | string | No | null | Crew description |
color | string | No | null | Palette ID (blue, emerald, violet, amber, rose, cyan, lime, fuchsia) |
icon | string | No | null | Lucide icon name (code, rocket, clipboard, etc.) |
container_memory_mb | integer | No | 4096 | Container memory limit in MB |
container_cpus | number | No | 2.0 | Container CPU limit |
container_ttl_hours | integer | No | null | Container time-to-live in hours |
network_mode | string | No | "free" | "free" or "restricted" |
allowed_domains | string[] | No | [] | Allowed domains when network_mode is "restricted" (ignored otherwise) |
runtime_image | string | No | null | Custom runtime image; existence is validated against the registry before insert |
devcontainer_config | string | No | null | Raw devcontainer.json (max 100 KB; parsed for syntax) |
mise_config | string | No | null | Raw mise config TOML (max 10 KB; parsed for syntax) |
services_json | string | No | null | Sidecar services JSON (max 64 KB; structurally validated) |
{
"name": "Engineering",
"slug": "engineering",
"description": "Core engineering team",
"color": "blue",
"icon": "code",
"container_memory_mb": 4096,
"container_cpus": 2.0,
"network_mode": "restricted",
"allowed_domains": ["api.github.com", "registry.npmjs.org"]
}
Response: 201 Created — returns the created crew object (same shape as List response item).
Error Responses:
| Status | Condition |
|---|
400 | Invalid name/slug length or format, invalid network_mode, invalid domain entry, runtime_image not found in registry, oversize or invalid devcontainer_config/mise_config/services_json |
402 | License crew limit exceeded |
403 | Insufficient role |
409 | Slug already taken in this workspace |
Get Crew
GET /api/v1/crews/{crewId}?workspace_id={workspaceId}
Auth: Session or CLI token + workspace membership
| Path Parameter | Description |
|---|
crewId | Crew ID |
Response: 200 OK — full crew object including _count, issue_prefix, mcp_config_json, and escalation_config.
| Status | Condition |
|---|
404 | Crew not found or does not belong to workspace |
Update Crew
PATCH /api/v1/crews/{crewId}?workspace_id={workspaceId}
PUT /api/v1/crews/{crewId}?workspace_id={workspaceId}
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.
| Field | Type | Description |
|---|
name | string | Display name (2-100 characters) |
slug | string | URL-safe identifier (2-50 characters) |
description | string | Crew description |
color | string | Palette ID |
icon | string | Lucide icon name |
avatar_style | string | Avatar style for agents |
container_memory_mb | integer | Container memory limit in MB |
container_cpus | number | Container CPU limit |
container_ttl_hours | integer | Container TTL in hours (0 clears it to null; negative is rejected) |
max_ephemeral_agents | integer | Per-crew hire-flow quota; must be between 0 and 100 |
network_mode | string | "free" or "restricted" |
allowed_domains | string[] | Allowed domains (restricted mode) |
mcp_config_json | string | Raw MCP server config JSON (must contain mcpServers object) |
escalation_config | string | Escalation policy config JSON (thresholds 0-1, auto_approve_threshold > require_approval_below) |
issue_prefix | string | Issue identifier prefix (e.g., "ENG"); empty string clears |
runtime_image | string | Custom runtime image (empty string clears; existence validated) |
devcontainer_config | string | Raw devcontainer.json (empty string clears; max 100 KB) |
mise_config | string | Raw mise config TOML (empty string clears; max 10 KB) |
services_json | string | Sidecar services JSON (empty/whitespace clears; max 64 KB) |
Response: 200 OK — updated crew object.
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.
| Status | Condition |
|---|
400 | Invalid field values (name/slug length, slug format, network_mode, domain entry, container_ttl_hours negative, max_ephemeral_agents outside 0-100, MCP JSON missing mcpServers, escalation thresholds out of range, oversize or invalid devcontainer_config/mise_config/services_json, runtime_image not found) |
403 | Insufficient role |
404 | Crew not found |
409 | Slug already taken |
Delete Crew
DELETE /api/v1/crews/{crewId}?workspace_id={workspaceId}
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.
Auth: Session or CLI token + OWNER or ADMIN role
Response: 200 OK
| Status | Condition |
|---|
403 | Insufficient role |
404 | Crew not found |
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
GET /api/v1/crews/{crewId}/members?workspace_id={workspaceId}
Response: 200 OK — array of member objects.
Add Member
POST /api/v1/crews/{crewId}/members?workspace_id={workspaceId}
Auth: OWNER, ADMIN, or MANAGER role
Request Body:
| Field | Type | Required | Description |
|---|
user_id | string | Yes | Target user’s ID (must already be a workspace member) |
role | string | No | Optional per-crew role override — one of OWNER, ADMIN, MANAGER, MEMBER, VIEWER. Omit to inherit the workspace role. |
{
"user_id": "user_abc123",
"role": "MANAGER"
}
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).
| Status | Condition |
|---|
400 | Missing user_id, invalid role, or user is not a workspace member |
404 | Crew not found |
409 | User is already a member of this crew |
Update Member Role
PATCH /api/v1/crews/{crewId}/members/{memberId}?workspace_id={workspaceId}
Content-Type: application/json
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:
| Field | Type | Required | Description |
|---|
role | string | No | One of OWNER, ADMIN, MANAGER, MEMBER, VIEWER. Omit or pass "" to clear the override (member inherits workspace role). |
Response: 200 OK — a status envelope echoing the applied role (an empty role means the override was cleared):
{ "status": "updated", "role": "ADMIN" }
| Status | Condition |
|---|
400 | role is not in the enum set |
403 | Caller is not workspace OWNER / ADMIN |
404 | Crew or member not found |
Remove Member
DELETE /api/v1/crews/{crewId}/members/{memberId}?workspace_id={workspaceId}
Auth: OWNER or ADMIN role
Response: 200 OK
| Status | Condition |
|---|
404 | Crew or member not found |
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
GET /api/v1/crews/{crewId}/persona
Response: 200 OK
{
"crew_id": "crew-uuid",
"layer": "crew",
"content": "# Default persona for this crew...\n",
"bytes": 412,
"cap_bytes": 1500
}
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
PUT /api/v1/crews/{crewId}/persona
Content-Type: application/json
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:
{ "content": "# Default persona for this crew\n\n..." }
Response: 200 OK — { "layer": "crew", "bytes": N, "updated": "<rfc3339>" }
| Status | Condition |
|---|
413 | content exceeds cap_bytes (1500) |
404 | Crew not found |
500 | Storage write / fsync failure |
Reset Crew Persona
DELETE /api/v1/crews/{crewId}/persona
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.
| Field | Values | Meaning |
|---|
autonomy_level | strict / guided / trusted / full | How much an agent can do without operator approval. strict queues everything; full auto-applies most decisions. |
behavior_mode | warn / block | What the behavior monitor does on a flagged action — warn lets it through with a journal entry, block rejects. |
Get Crew Policy
GET /api/v1/crews/{crewId}/policy
Response: 200 OK
{
"crew_id": "crew-uuid",
"autonomy_level": "guided",
"behavior_mode": "warn",
"set_by_user_id": "user-uuid",
"set_at": "2026-05-26T14:22:10Z",
"reason": "Default for newly-onboarded crews"
}
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
PUT /api/v1/crews/{crewId}/policy
Content-Type: application/json
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:
{
"autonomy_level": "trusted",
"behavior_mode": "block",
"reason": "Crew has cleared 30 days of approvals without an override"
}
| Field | Required | Notes |
|---|
autonomy_level | yes | Must be one of the four enum values; other strings return 400. |
behavior_mode | yes | warn or block. Missing field returns 400 (no silent default). |
reason | conditional | Required only when autonomy_level=full — non-empty, persisted on the audit triple. Optional for strict / guided / trusted. |
Response: 200 OK — same shape as GET, reflecting the new
state and the freshly-recorded audit triple.
| Status | Condition |
|---|
400 | Invalid enum value, the forbidden full + block combination, or reason missing when autonomy_level=full |
404 | Crew not found |
Avatar
Bulk-apply an avatar style across a crew’s agents, or clear per-agent overrides.
Apply Avatar Style
POST /api/v1/crews/{crewId}/apply-avatar-style?workspace_id={workspaceId}
Applies an avatar_style to all non-deleted agents in the crew, or clears per-agent overrides.
Auth: OWNER or ADMIN role
Request Body:
| Field | Type | Required | Description |
|---|
avatar_style | string | Conditional | Style to apply (required unless reset_overrides is true) |
reset_overrides | boolean | No | When true, clears avatar_style on every agent (ignores avatar_style) |
Response: 200 OK
{ "updated": 3, "style": "bottts" }
When reset_overrides is true, the response is { "updated": N, "reset": true }.
| Status | Condition |
|---|
400 | avatar_style missing and reset_overrides not set |
404 | Crew not found |
Connections
Crew connections define which crews can communicate with each other for cross-crew task assignment and messaging.
List Connections
GET /api/v1/crew-connections?workspace_id={workspaceId}
Response: 200 OK — array of connection objects:
[
{
"id": "cc_abc",
"workspace_id": "ws_123",
"from_crew_id": "crew_abc",
"from_crew_name": "Engineering",
"from_crew_slug": "engineering",
"to_crew_id": "crew_xyz",
"to_crew_name": "Design",
"to_crew_slug": "design",
"direction": "bidirectional",
"status": "active",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}
]
Create Connection
POST /api/v1/crew-connections?workspace_id={workspaceId}
Auth: OWNER, ADMIN, or MANAGER role
Request Body:
| Field | Type | Required | Default | Description |
|---|
from_crew_id | string | Yes | — | Source crew ID |
to_crew_id | string | Yes | — | Target crew ID (must differ from from_crew_id) |
direction | string | No | "bidirectional" | "bidirectional" or "unidirectional" |
{
"from_crew_id": "crew_abc",
"to_crew_id": "crew_xyz",
"direction": "bidirectional"
}
Response: 201 Created
| Status | Condition |
|---|
400 | Missing IDs, same source/target, or invalid direction |
404 | One or both crews not found in this workspace |
409 | Connection already exists (or other UNIQUE/constraint violation) |
Delete Connection
DELETE /api/v1/crew-connections/{connectionId}?workspace_id={workspaceId}
Auth: OWNER, ADMIN, or MANAGER role
Response: 204 No Content
| Status | Condition |
|---|
404 | Connection not found |
Crew Files (Proxy)
File operations are proxied through the crewshipd sidecar to the crew’s container filesystem.
| Endpoint | Method | Description |
|---|
GET /api/v1/crews/{crewId}/files | GET | List files in crew workspace |
GET /api/v1/crews/{crewId}/files/download?path=... | GET | Download a file |
PUT /api/v1/crews/{crewId}/files/save | PUT | Save/upload a file |
Crew Assignments
GET /api/v1/crews/{crewId}/assignments?workspace_id={workspaceId}
List task assignments for a crew.
Response: 200 OK — array of assignment objects.
Crew Peer Conversations
GET /api/v1/crews/{crewId}/peer-conversations?workspace_id={workspaceId}
List peer-to-peer conversations between agents in a crew.
Crew Standup
GET /api/v1/crews/{crewId}/standup?workspace_id={workspaceId}
Get standup summary for a crew.
Crew Escalations
List Escalations
GET /api/v1/crews/{crewId}/escalations?workspace_id={workspaceId}
Resolve Escalation
PATCH /api/v1/escalations/{escalationId}/resolve?workspace_id={workspaceId}
Pending Escalation Count
GET /api/v1/escalations/pending-count?workspace_id={workspaceId}
Returns count of pending escalations across the workspace.