An agent is an AI coding agent with a configurable role, CLI adapter, skills, and credentials. Agents belonging to the same crew share one Linux container. These endpoints cover the full agent lifecycle: creating and configuring permanent agents, hiring short-lived ephemeral “contractors”, managing their skills and credentials, driving chats and runs, inspecting workspace files, and tuning the persona and self-learning layers that shape each agent’s behavior.
All /api/v1/agents/* endpoints require authentication and workspace context (the workspace_id query parameter). The chat-message proxy (/api/v1/chats/{chatId}/messages) is the exception — it resolves the workspace from the chat row and takes no workspace_id.
Endpoints
| Method | Endpoint | Purpose |
|---|
| GET | /api/v1/agents/crews-status | Lightweight agent counts by status for toolbar/dashboard widgets |
| GET | /api/v1/agent-load | Agent load metrics across the workspace |
| GET | /api/v1/agents | List agents (optionally filtered by crew) |
| POST | /api/v1/agents | Create a new agent |
| GET | /api/v1/agents/ | Get a single agent |
| PATCH | /api/v1/agents/ | Partially update an agent |
| DELETE | /api/v1/agents/ | Soft-delete an agent |
| POST | /api/v1/agents/hire | Spawn a new ephemeral agent under a crew |
| POST | /api/v1/agents//rehire | Resurrect or extend an ephemeral agent |
| POST | /api/v1/agents//approve-hire | Approve a guided-autonomy pending hire |
| GET | /api/v1/agents//persona | Get the resolved persona |
| PUT | /api/v1/agents//persona | Operator-only direct persona write |
| DELETE | /api/v1/agents//persona | Remove the agent persona layer |
| GET | /api/v1/agents//persona/history | List persona version history |
| POST | /api/v1/agents//persona/suggest | Agent-initiated persona proposal |
| GET | /api/v1/agents//learning | Get the self-learning flag and audit triple |
| PATCH | /api/v1/agents//learning | Flip the self-learning flag |
| GET | /api/v1/agents//inbox | Consolidated “what’s waiting on this agent” payload |
| GET | /api/v1/agents//peers | List the agent’s peer-card index |
| GET | /api/v1/agents//peers/ | Get one peer card’s full content |
| DELETE | /api/v1/agents//peers/ | Delete one peer card |
| GET | /api/v1/agents//skills | List skill assignments |
| POST | /api/v1/agents//skills | Assign a skill |
| DELETE | /api/v1/agents//skills/ | Remove a skill |
| GET | /api/v1/agents//credentials | List credential assignments |
| POST | /api/v1/agents//credentials | Assign a credential |
| DELETE | /api/v1/agents//credentials/ | Remove a credential |
| GET | /api/v1/agents//chats | List chat sessions |
| POST | /api/v1/agents//chats | Create a new chat session |
| GET | /api/v1/agents//runs | List execution history |
| GET | /api/v1/agents//debug | Debug information for a running agent |
| GET | /api/v1/agents//files | List files in agent workspace |
| GET | /api/v1/agents//files/download | Download a file |
| PUT | /api/v1/agents//files/save | Save/upload a file |
| GET | /api/v1/agents//container-files | List files inside the running container |
| GET | /api/v1/agents//git-log | Recent git commits from the container workspace |
| GET | /api/v1/agents//logs | Get agent logs |
| POST | /api/v1/agents//stop | Stop a running agent |
| GET | /api/v1/chats//messages | Get messages for a chat session |
Status & Metrics
Lightweight, read-only aggregates that power toolbar and dashboard widgets.
Crews Status
GET /api/v1/agents/crews-status?workspace_id={workspaceId}
# Response is a per-workspace aggregate counting agents by status
# (IDLE/RUNNING/ERROR — no STOPPED bucket). Drives the 'Crews idle/online'
# badge in the top toolbar.
Returns lightweight agent counts by status for toolbar/dashboard widgets.
Response: 200 OK
{
"total": 12,
"running": 3,
"error": 1,
"idle": 8,
"queued": 0
}
The agent status buckets (total, running, error, idle) are always present — zero counts are not omitted. Any status that is not RUNNING or ERROR (including IDLE and PENDING_REVIEW) folds into the idle bucket; there is no stopped bucket. The queued field counts admission-queue assignments (not agents) currently in the QUEUED state — independent of the agent buckets, and 0 on servers that pre-date the queue migration (internal/api/agents.go:111-151).
Agent Load
GET /api/v1/agent-load?workspace_id={workspaceId}
Returns agent load metrics across the workspace.
Lifecycle & CRUD
Create, read, update, and soft-delete permanent agents.
List Agents
GET /api/v1/agents?workspace_id={workspaceId}
Query Parameters:
| Parameter | Type | Description |
|---|
workspace_id | string | Required. Workspace ID |
crew_id | string | Optional. Filter by crew |
Response: 200 OK
[
{
"id": "agent_abc",
"crew_id": "crew_123",
"workspace_id": "ws_456",
"name": "Backend Dev",
"slug": "backend-dev",
"description": "Go backend developer",
"role_title": "Senior Developer",
"agent_role": "AGENT",
"lead_mode": null,
"status": "IDLE",
"cli_adapter": "CLAUDE_CODE",
"llm_provider": "ANTHROPIC",
"llm_model": "claude-sonnet-4-20250514",
"system_prompt": "You are a Go backend developer...",
"avatar_seed": "backend-dev",
"avatar_style": "bottts",
"timeout_seconds": 1800,
"tool_profile": "CODING",
"memory_enabled": true,
"cli_tools": null,
"schedule_cron": null,
"schedule_prompt": null,
"schedule_enabled": false,
"schedule_last_run": null,
"schedule_next_run": null,
"mcp_config_json": null,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z",
"crew": {
"name": "Engineering",
"slug": "engineering",
"color": "blue",
"avatar_style": "bottts"
},
"_count": {
"skills": 2,
"credentials": 1,
"chats": 5
},
"created_by_user_id": "user_789",
"ephemeral": false,
"expires_at": null,
"expired_at": null,
"parent_lead_id": null,
"hire_reason": null
}
]
mcp_config_json and created_by_user_id carry omitempty — they are dropped from the JSON entirely when null/empty rather than serialized as null.
Response Fields
| Field | Type | Description |
|---|
id | string | Agent ID |
crew_id | string | Parent crew ID |
workspace_id | string | Workspace ID |
name | string | Display name |
slug | string | URL-safe identifier |
description | string? | Agent description |
role_title | string? | Human-readable role title |
agent_role | string | AGENT or LEAD |
lead_mode | string? | "active" or "passive" (LEAD agents only) |
status | string | IDLE, RUNNING, or ERROR |
cli_adapter | string | CLI tool used to run the agent |
llm_provider | string? | LLM provider name |
llm_model | string? | LLM model identifier |
system_prompt | string? | System prompt for the agent |
avatar_seed | string? | Seed for avatar generation |
avatar_style | string? | Avatar style (e.g., "bottts", "pixel-art") |
timeout_seconds | integer | Maximum execution time |
tool_profile | string | Tool access profile |
memory_enabled | boolean | Whether agent memory is enabled |
cli_tools | string? | Comma-separated list of allowed CLI tools |
schedule_cron | string? | Cron expression for scheduled runs |
schedule_prompt | string? | Prompt used for scheduled runs |
schedule_enabled | boolean | Whether schedule is active |
schedule_last_run | string? | ISO 8601 timestamp of last scheduled run |
schedule_next_run | string? | ISO 8601 timestamp of next scheduled run |
mcp_config_json | string? | Agent-level MCP server config (omitted when empty) |
crew | object? | Embedded crew info (name, slug, color, avatar_style) |
_count | object | Counts: skills, credentials, chats |
created_by_user_id | string | Creator user ID; omitted for legacy rows with no creator |
ephemeral | boolean | true for hired contractor agents, false for permanent agents |
expires_at | string? | RFC3339 TTL deadline (ephemeral agents only) |
expired_at | string? | RFC3339 ghost timestamp once the TTL elapses |
parent_lead_id | string? | LEAD agent that parented this ephemeral |
hire_reason | string? | Timestamped hire/rehire reason log |
Create Agent
POST /api/v1/agents?workspace_id={workspaceId}
Auth: 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, lowercase letters/digits/hyphens) |
crew_id | string | Conditional | null | Parent crew ID. Required when agent_role is LEAD; optional for AGENT (internal/api/agents_create.go:73) |
description | string | No | null | Agent description |
role_title | string | No | null | Human-readable role title |
agent_role | string | No | "AGENT" | AGENT or LEAD |
lead_mode | string | No | "active" | "active" or "passive" (LEAD agents only) |
cli_adapter | string | No | "CLAUDE_CODE" | CLI adapter |
llm_provider | string | No | null | LLM provider |
llm_model | string | No | null | LLM model |
system_prompt | string | No | null | System prompt |
avatar_seed | string | No | null | Avatar seed |
avatar_style | string | No | null | Avatar style |
timeout_seconds | integer | No | 1800 | Max execution time in seconds |
tool_profile | string | No | "CODING" | Tool profile |
memory_enabled | boolean | No | false | Enable agent memory |
{
"name": "Backend Dev",
"slug": "backend-dev",
"crew_id": "crew_123",
"agent_role": "AGENT",
"cli_adapter": "CLAUDE_CODE",
"system_prompt": "You are a Go backend developer...",
"memory_enabled": true,
"tool_profile": "CODING"
}
Agent Roles:
| Role | Description | Crew Required |
|---|
AGENT | Worker that executes tasks | Yes |
LEAD | Crew leader that plans and delegates | Yes (max 1 per crew) |
CLI Adapters: CLAUDE_CODE, CODEX_CLI, GEMINI_CLI, OPENCODE, CURSOR_CLI, FACTORY_DROID
Tool Profiles: MINIMAL, CODING, FULL
Response: 201 Created — returns the created agent object.
| Status | Condition |
|---|
400 | Invalid name, slug, role, or missing required crew_id |
402 | License agent limit exceeded |
403 | Insufficient role |
409 | Slug already taken, or crew already has a LEAD agent |
Get Agent
GET /api/v1/agents/{agentId}?workspace_id={workspaceId}
Response: 200 OK — full agent object.
| Status | Condition |
|---|
404 | Agent not found |
Update Agent
PATCH /api/v1/agents/{agentId}?workspace_id={workspaceId}
Partial update — only provided fields are changed.
Auth: Per-agent edit gate (canEditAgent in internal/api/agents_update.go:27, defined at internal/api/rbac.go:151). OWNER / ADMIN may edit any agent; MANAGER may edit only agents they created or agents in crews where they hold per-crew ADMIN/OWNER; MEMBER / VIEWER are refused.
Request Body: Same fields as Create, all optional. status is not mutable through this endpoint. Additionally supports:
| Field | Type | Description |
|---|
cli_tools | string | Comma-separated CLI tools |
schedule_cron | string | Cron expression |
schedule_prompt | string | Schedule prompt |
schedule_enabled | boolean | Enable/disable schedule |
mcp_config_json | string | Agent MCP config JSON (must contain an mcpServers object) |
Response: 200 OK — updated agent object.
Delete Agent
DELETE /api/v1/agents/{agentId}?workspace_id={workspaceId}
Soft-deletes the agent (sets deleted_at).
This is a destructive operation. Same per-agent edit gate as Update (canEditAgent in internal/api/agents_query.go:266, defined at internal/api/rbac.go:151). OWNER / ADMIN delete any agent; MANAGER deletes only agents they created or agents in crews where they hold per-crew ADMIN/OWNER.
Response: 200 OK
Hire / Rehire / Approve
Ephemeral agents (“contractors”) are short-lived agents spawned against a
crew with a TTL. When the TTL elapses the agent becomes a ghost (the
row is preserved for audit but no longer counts against the crew’s quota).
The hire flow is gated by the crew’s autonomy policy (strict /
guided / trusted / full) and bounded by the crew’s
max_ephemeral_agents quota (ghosts excluded).
All three endpoints require OWNER, ADMIN, or MANAGER role (canRole(role, "create")).
Hire Agent
POST /api/v1/agents/hire?workspace_id={workspaceId}
Spawns a new ephemeral agent under a crew. The crew’s autonomy policy
decides the outcome:
| Autonomy | Outcome |
|---|
strict | 403 — the hire is rejected with a structured reason. |
guided | 202 — the agent row is created in PENDING_REVIEW, a blocking inbox waitpoint is dropped, and the agent will not start until approved. |
trusted | 201 — the agent goes live immediately; a non-blocking inbox visibility item is recorded. |
full | 201 — the agent goes live immediately; journal-only, no inbox row. |
Auth: OWNER, ADMIN, or MANAGER role
Request Body:
| Field | Type | Required | Description |
|---|
crew_id | string | Conditional | Target crew by id. Provide exactly one of crew_id / crew_slug. |
crew_slug | string | Conditional | Target crew by slug. Mutually exclusive with crew_id. |
template_slug | string | Yes | Crew template the ephemeral is built from (built-in or workspace-owned). |
ttl_minutes | integer | No | Lifetime in minutes. Clamped to 1–1440; a value of 0/negative falls back to the server floor of 30. |
reason | string | Yes | Audit/history trail for the hire. |
model | string | No | LLM model override. Empty falls back to the template default at provisioning time. |
parent_lead_id | string | No | A LEAD agent in the same crew + workspace that parents this ephemeral. |
{
"crew_slug": "engineering",
"template_slug": "backend-dev",
"ttl_minutes": 60,
"reason": "Spike: investigate the flaky deploy script",
"model": "claude-sonnet-4-20250514"
}
Response: 201 Created (live) or 202 Accepted (pending review).
{
"id": "agent_abc",
"crew_id": "crew_123",
"workspace_id": "ws_456",
"slug": "backend-dev-eph-1a2b3c",
"name": "Backend Dev",
"status": "IDLE",
"ephemeral": true,
"expires_at": "2026-06-04T13:00:00Z",
"expired_at": null,
"parent_lead_id": null,
"hire_reason": "[2026-06-04T12:00:00Z] hire: Spike: investigate the flaky deploy script",
"pending_review": false,
"inbox_item_id": "ib-cuid",
"decision": "auto_log_journal"
}
| Field | Type | Description |
|---|
id | string | New agent id. |
crew_id | string? | Resolved crew id. |
workspace_id | string | Workspace id. |
slug | string | Generated ephemeral slug (<template>-eph-<hex>). |
name | string | Template name. |
status | string | IDLE on a live hire, PENDING_REVIEW when the policy is guided. |
ephemeral | boolean | Always true. |
expires_at | string? | RFC3339 TTL deadline. |
expired_at | string? | RFC3339 ghost timestamp (always null on a fresh hire). |
parent_lead_id | string? | Echo of the parenting LEAD, when supplied. |
hire_reason | string? | Timestamped reason log (rehires append to this). |
pending_review | boolean | true only on the guided → 202 path. |
inbox_item_id | string | Inbox row id (blocking waitpoint on guided, visibility item on trusted). Omitted when empty (full). |
decision | string | The policy decision that produced this outcome. |
| Status | Condition |
|---|
201 | Live ephemeral (trusted / full). |
202 | PENDING_REVIEW + blocking inbox item (guided). |
400 | Missing/invalid template_slug, reason, or crew_id/crew_slug (neither, or both). |
403 | strict autonomy rejected the hire, or caller lacks MANAGER+. |
404 | Crew or template not found in this workspace. |
429 | Crew quota (max_ephemeral_agents) reached — rehire a ghost or raise the quota. |
Credential assignment
On a successful hire (201/202), Crewship auto-assigns the available
workspace Anthropic credentials (API_KEY / AI_CLI_TOKEN, first-created
wins) to the new agent so it can authenticate on first run. This is
best-effort and runs after the agent row is committed: assignment failures are
journaled as credential.auto_assign_failed and never fail the hire, and a
workspace with no Anthropic credential journals credential.auto_assign_empty.
Assign manually later via POST /api/v1/agents/{agentId}/credentials if needed.
Rehire Agent
POST /api/v1/agents/{agentId}/rehire?workspace_id={workspaceId}
Resurrects an expired ephemeral (“ghost”): clears expired_at, pushes
expires_at forward by the new TTL, and appends a new reason line to
the hire_reason history. The container is not rebuilt here — the
chatbridge auto-provisions a fresh one on the next message. Rehiring a
still-live ephemeral (extending its TTL before it ghosts) is free and does
not consume an extra quota slot.
Auth: OWNER, ADMIN, or MANAGER role
Request Body:
| Field | Type | Required | Description |
|---|
ttl_minutes | integer | No | New lifetime in minutes. Same 1–1440 clamp / 30-minute floor as Hire. |
reason | string | Yes | Appended to the hire_reason history trail. |
{
"ttl_minutes": 90,
"reason": "Deploy script needs another pass"
}
Response: 200 OK — same shape as the Hire response, echoing the
agent’s persisted status (rehire does not change it) and the updated
expires_at / hire_reason.
| Status | Condition |
|---|
200 | Rehire succeeded; agent is live again. |
400 | Missing reason, or invalid JSON. |
403 | Caller lacks MANAGER+, or strict autonomy rejected the rehire. |
404 | Agent not found in this workspace, or the agent is not ephemeral. |
429 | Crew quota reached (only when rehiring a ghost). |
Approve Hire
POST /api/v1/agents/{agentId}/approve-hire?workspace_id={workspaceId}
The guided-autonomy approval step. Flips a PENDING_REVIEW ephemeral to
IDLE (an atomic conditional UPDATE, so two concurrent approvals can’t
both win), resolves the blocking inbox waitpoint addressed to the agent,
and writes an agent.hire_approved audit entry. The chatbridge refuses to
start a PENDING_REVIEW agent, so this is what actually releases the gate.
Auth: OWNER, ADMIN, or MANAGER role
Request Body: empty.
Response: 200 OK
{
"id": "agent_abc",
"status": "IDLE",
"crew_id": "crew_123"
}
| Status | Condition |
|---|
200 | Agent flipped to IDLE; chatbridge will now serve messages. |
403 | Caller lacks MANAGER+. |
404 | Agent not found in this workspace. |
409 | Agent is not in PENDING_REVIEW (already approved, hired under non-guided autonomy, or a permanent agent). |
Agent Persona
The persona layer is per-agent PERSONA.md markdown that the
orchestrator injects into every system prompt. Resolution is layered:
the agent layer (if a file exists on disk) wins; otherwise the crew
default layer; otherwise the synthesized default built from the
agent’s role + role title. Direct writes are operator-only; agents
edit only via the suggest endpoint, which the policy resolver
gates per crew-autonomy level (see Autonomy & self-learning).
Get Persona
GET /api/v1/agents/{agentId}/persona
Returns the resolved persona — agent layer if a file is present,
crew default otherwise, synthesised default if neither layer is
configured.
Response: 200 OK
{
"agent_id": "agent-uuid",
"layer": "agent",
"from_default": false,
"content": "# PERSONA.md...\n",
"bytes": 1842,
"cap_bytes": 1500
}
| Field | Type | Description |
|---|
layer | string | agent or crew — which layer the response came from. The synthesised default is reported as crew with from_default = true. |
from_default | bool | true when no file exists on either layer and the synthesised “You are the …” stub was returned |
cap_bytes | integer | Max accepted size on PUT / POST suggest (memory.PersonaCapBytes = 1500 bytes) |
Set Persona
PUT /api/v1/agents/{agentId}/persona
Content-Type: application/json
Operator-only direct write of the agent layer. Records a row in
memory_versions so Get Persona History can
replay the chain.
Request:
{ "content": "# Engineer\n\nFocus on backend services...\n" }
Response: 200 OK
{ "layer": "agent", "bytes": 1842, "updated": "2026-05-27T18:42:11Z" }
| Status | Condition |
|---|
413 | content exceeds cap_bytes |
400 | Invalid JSON or missing storage configuration |
500 | Storage write/fsync failure |
Agents cannot use this endpoint to mutate their own persona — ActionPersonaDirectWrite is DecisionRejected across every autonomy level in Phase 1. Agent edits flow exclusively through Suggest Persona.
Reset Persona
DELETE /api/v1/agents/{agentId}/persona
Removes the agent layer file. The next GET falls through to the crew layer (or the synthesised default).
Response: 204 No Content
Get Persona History
GET /api/v1/agents/{agentId}/persona/history?limit=20
Lists rows from memory_versions filtered to this agent’s
PERSONA.md path. Pairs with GET /api/v1/admin/memory/versions/{id}/content for content drill-down — the history endpoint deliberately omits content to keep responses small.
Query Parameters:
| Param | Default | Max | Description |
|---|
limit | 20 | 100 | Page size, clamped server-side. |
Response: 200 OK
{
"entries": [
{
"id": "mv-cuid",
"sha256": "deadbeef…",
"bytes": 1842,
"written_at": "2026-05-27T18:42:11Z",
"written_by": "user-uuid",
"parent_sha": "cafef00d…"
}
]
}
parent_sha is omitted on the first version (when the chain starts).
Suggest Persona
POST /api/v1/agents/{agentId}/persona/suggest
Content-Type: application/json
Agent-initiated persona proposal. The crew’s autonomy policy decides
the outcome:
| Crew autonomy | Outcome |
|---|
strict / guided / trusted | Inbox approval item created. Agent learns pending so it can reference the proposal in subsequent runs. |
full | Auto-apply + journal entry — unless the agent’s self_learning_enabled = 0, which demotes auto-apply back to inbox approval. |
ActionPersonaDirectWrite is rejected across every autonomy level —
this endpoint is the only path an agent can use to write a
persona.
Request:
{
"content": "# Engineer\n\nFocus on backend services...\n",
"rationale": "Operator briefly emphasised the on-call rotation; reflecting that in the persona."
}
Response: 200 OK (proposal queued or applied) / 403 (policy rejected)
{
"agent_id": "agent-uuid",
"decision": "inbox_approve",
"bytes": 1842,
"rationale": "...",
"timestamp": "2026-05-27T18:42:11Z",
"applied": false,
"pending": true
}
| Field | Type | Description |
|---|
decision | string | Policy decision that drove the outcome (e.g. inbox_approve, auto_journal, rejected). |
applied | bool | true only when the suggestion was auto-applied to disk (full autonomy + self_learning_enabled = 1). |
pending | bool | true when the proposal was queued for operator approval (present on the inbox path). |
self_learning_gate | string? | "off" only when a per-agent self_learning_enabled = 0 demoted an auto-apply decision back to inbox approval. |
There is no inbox_id in this response — the proposal is recorded as an audit_logs row (persona.suggest_pending), not returned to the caller.
| Status | Condition |
|---|
400 | Missing / empty content field |
403 | Policy rejected the suggestion (autonomy floor) |
413 | content exceeds cap_bytes |
503 | Server built without storage configuration (outputBasePath empty) |
Agent Self-Learning Posture
Per-agent flag controlling whether ALLOW decisions from the Keeper
evaluators auto-apply (enabled = true) or queue for operator
approval in the inbox (enabled = false). Orthogonal to the crew’s
autonomy_level: the per-action policy gate (policy.DecideAction)
is the upstream authority — this flag only decides what happens to
already-ALLOWed decisions. See Autonomy & self-learning.
Get Self-Learning
GET /api/v1/agents/{agentId}/learning
Returns the current flag plus the audit triple (who flipped it, when,
why). Any authenticated workspace member can read — the value is
non-secret diagnostic state.
Response: 200 OK
{
"agent_id": "agent-uuid",
"enabled": true,
"set_by_user_id": "user-uuid",
"set_at": "2026-05-26T09:14:22Z",
"reason": "Trusted crew, agent has cleared 50+ approvals without an override"
}
| Field | Type | Notes |
|---|
enabled | bool | Persisted as 0 / 1 on agents.self_learning_enabled. |
set_by_user_id | string? | Omitted on agents that have never had the flag flipped. |
set_at | string? | RFC3339; omitted when never set. |
reason | string? | Operator-supplied rationale from the most recent PATCH; omitted when never set. |
Set Self-Learning
PATCH /api/v1/agents/{agentId}/learning
Content-Type: application/json
Flip the flag. Requires OWNER / ADMIN (canRole(role, "manage"))
— self-learning weakens the inbox-approval invariant, so the operator
who turns it on must be senior enough to own the consequences.
Request:
{
"enabled": true,
"reason": "Trusted crew, agent has cleared 50+ approvals without an override"
}
| Field | Type | Required | Notes |
|---|
enabled | bool | yes | Decoded as *bool server-side — a missing or null field returns 400, not a silent flip to false. Be explicit about direction. |
reason | string | yes | Non-empty after trim. Persisted on the row so a later audit can answer “who turned this on, when, and why”. |
Response: 200 OK — same shape as GET, reflecting the new state and the freshly-recorded audit triple.
| Status | Condition |
|---|
400 | enabled field missing / null, or reason empty |
403 | Caller is not OWNER / ADMIN |
404 | Agent not found in the current workspace |
Agent Inbox Summary
GET /api/v1/agents/{agentId}/inbox
Consolidated “what’s waiting on this agent” payload used by the
Crews preview panel — one round-trip instead of four parallel fetches
across approvals, assignments, escalations, and peer messages. UI is
the primary consumer; operators can poll the endpoint for an at-a-glance
load check.
Response: 200 OK
{
"approvals_pending": 2,
"assignments_open": 5,
"escalations_open": 0,
"peer_messages": [
{
"id": "pm-uuid",
"from_agent_name": "Martin",
"from_agent_slug": "martin",
"question": "What's the latest on the deploy script?",
"response": null,
"escalated": false,
"status": "open",
"created_at": "2026-05-27T12:30:00Z",
"direction": "incoming"
}
],
"cost_usd_this_month": 4.21,
"llm_calls_this_month": 312,
"tokens_used_this_month": 184221
}
| Field | Type | Description |
|---|
approvals_pending | int | Inbox waitpoint items awaiting this agent’s decision. |
assignments_open | int | Tasks routed to this agent that have not reached a terminal state. |
escalations_open | int | Peer conversations escalated up the lead chain that still target this agent. |
peer_messages | array | Last N peer messages (both directions). direction is "incoming" or "outgoing"; response and duration_ms are populated only for resolved exchanges. |
cost_usd_this_month | float | Sum of cost_ledger.cost_usd for this agent in the current calendar month. 0 when the cost_ledger table is absent (older workspaces). |
llm_calls_this_month | int | Count of cost_ledger rows for this agent in the current month. |
tokens_used_this_month | int64 | Sum of input_tokens + output_tokens from cost_ledger in the current month. |
| Status | Condition |
|---|
401 | Workspace context missing |
404 | Agent not found in the current workspace |
Agent Peer Cards
Per-(agent, user) markdown notes produced by the PeerCardSync
routine — the agent’s own “what I know about this person” file.
These endpoints are operator-facing: the cards live on disk
under {outputBase}/crews/{crewID}/agents/{slug}/.memory/peers/,
and every read/write/delete writes a peer_card_audit row for GDPR
SAR coverage.
The user-facing surface (view-mine / opt-out / delete-mine) lives
under /api/v1/users/me/peer-cards and shares the same disk + DB
primitives — these agent-flavor endpoints exist so an operator can
clean up cards on behalf of a user (compliance ticket, leaver
workflow) or inspect what an agent has been writing about people.
The {userId} path parameter is the raw user_id; the server
derives the user_slug via memory.UserSlug so the URL stays
debuggable.
List Agent Peers
GET /api/v1/agents/{agentId}/peers
Returns the agent’s peer-card index (no card content — fetch via the
single-card endpoint below). Workspace-scoped; agents in other
workspaces return 404.
Response: 200 OK
{
"agent_id": "agent-uuid",
"peers": [
{
"id": "pc-uuid",
"user_id": "user-uuid",
"user_slug": "alice",
"bytes": 412,
"created_at": "2026-05-10T...",
"updated_at": "2026-05-26T..."
}
]
}
| Status | Condition |
|---|
404 | Agent not found in the current workspace |
409 | Agent has no crew_id — peer cards require a crew-scoped path |
Get Agent Peer Card
GET /api/v1/agents/{agentId}/peers/{userId}
Returns one card’s full content (markdown). Writes a read row to
peer_card_audit.
Response: 200 OK
{
"agent_id": "agent-uuid",
"user_id": "user-uuid",
"user_slug": "alice",
"content": "# Alice Chen\n\n- Prefers async over sync...\n",
"bytes": 412
}
| Status | Condition |
|---|
404 | Agent not found, or no card exists for this (agent, user) |
409 | Agent has no crew_id |
503 | outputBasePath not configured server-side |
Delete Agent Peer Card
DELETE /api/v1/agents/{agentId}/peers/{userId}
Removes the card from disk and the peer_cards row, then writes a delete audit row. Idempotent — deleting a non-existent card returns 204 rather than 404 so an operator script can retry safely.
Response: 204 No Content
The GDPR cascade endpoint DELETE /api/v1/admin/users/{userId}/data deletes peer cards across all agents in the workspace for a single user — that’s the right path for a full SAR. This per-agent endpoint is for the narrower “clean up just this agent’s notes about this user” case.
Agent Skills
Assign, list, and remove the skills available to an agent.
List Skills
GET /api/v1/agents/{agentId}/skills?workspace_id={workspaceId}
Response: 200 OK — array of skill assignment objects.
Add Skill
POST /api/v1/agents/{agentId}/skills?workspace_id={workspaceId}
Auth: OWNER, ADMIN, or MANAGER role (canRole(role, "create"))
Request Body:
| Field | Type | Required | Description |
|---|
skill_id | string | Yes | Skill to assign |
config | string | No | Per-assignment config blob (stored as-is) |
{
"skill_id": "skill_abc"
}
Response: 201 Created — { "id": "<assignment_id>" } on a fresh assign.
The assign is idempotent: re-assigning an already-installed skill returns 200 OK with { "id": "<existing_id>", "already_assigned": true } rather than a 409.
Remove Skill
DELETE /api/v1/agents/{agentId}/skills/{skillId}?workspace_id={workspaceId}
Response: 204 No Content
Agent Credentials
Assign, list, and remove the credentials injected into an agent’s container.
List Credentials
GET /api/v1/agents/{agentId}/credentials?workspace_id={workspaceId}
Response: 200 OK — array of credential assignment objects.
Assign Credential
POST /api/v1/agents/{agentId}/credentials?workspace_id={workspaceId}
Auth: OWNER or ADMIN role (canRole(role, "manage") in internal/api/agent_credentials.go:89)
Request Body:
| Field | Type | Required | Description |
|---|
credential_id | string | Yes | Credential to assign (must exist in this workspace) |
env_var_name | string | Yes | Environment variable name to inject into the agent container |
priority | integer | No | Resolution priority when multiple credentials share an env_var_name |
{
"credential_id": "cred_abc",
"env_var_name": "ANTHROPIC_API_KEY",
"priority": 0
}
Response: 201 Created
{ "id": "<assignment_id>" }
| Status | Condition |
|---|
400 | Missing credential_id or env_var_name |
403 | Insufficient role |
404 | Agent or credential not found |
409 | Credential already assigned to this agent |
Remove Credential
DELETE /api/v1/agents/{agentId}/credentials/{assignmentId}?workspace_id={workspaceId}
The path segment is the assignment ID (agent_credentials.id), not the credential id.
Auth: OWNER or ADMIN role (canRole(role, "manage") in internal/api/agent_credentials.go:146)
Response: 200 OK
Agent Chats & Runs
List and create interactive chat sessions, and review execution history.
List Chats
GET /api/v1/agents/{agentId}/chats?workspace_id={workspaceId}
Response: 200 OK — array of chat session objects.
Create Chat
POST /api/v1/agents/{agentId}/chats?workspace_id={workspaceId}
Creates a new interactive chat session with the agent.
Response: 201 Created
Agent Runs
GET /api/v1/agents/{agentId}/runs?workspace_id={workspaceId}
List execution history for an agent.
Response: 200 OK — array of run objects.
Agent Proxy Endpoints
These endpoints proxy requests through the crewshipd sidecar to the agent’s container.
POST /api/v1/agents/{agentId}/stop halts a running agent.
| Endpoint | Method | Description |
|---|
GET /api/v1/agents/{agentId}/debug | GET | Debug information for a running agent |
GET /api/v1/agents/{agentId}/files | GET | List files in agent workspace |
GET /api/v1/agents/{agentId}/files/download?path=... | GET | Download a file |
PUT /api/v1/agents/{agentId}/files/save | PUT | Save/upload a file |
GET /api/v1/agents/{agentId}/container-files | GET | List files inside the running container (see below) |
GET /api/v1/agents/{agentId}/git-log | GET | Recent git commits from the container workspace (see below) |
GET /api/v1/agents/{agentId}/logs | GET | Get agent logs |
POST /api/v1/agents/{agentId}/stop | POST | Stop a running agent |
Container Files
GET /api/v1/agents/{agentId}/container-files?subdir={path}
Lists files inside the agent’s running container (as opposed to /files, which lists the persistent workspace mount). Proxied through crewshipd; the request is rejected before the IPC hop if the agent is not assigned to a crew. (internal/api/proxy_files.go:350)
Auth: Session or CLI token + workspace membership with at least read-tier role (canRole(role, "read"))
Query Parameters:
| Parameter | Type | Description |
|---|
subdir | string | Optional sub-path within the container’s working directory (normalized to reject .. traversal) |
Response: 200 OK — JSON array of file entries (unwrapped from the IPC layer’s {files: [...]} envelope). Returns [] when the sidecar response is unparseable or empty.
| Status | Condition |
|---|
400 | subdir path is invalid (traversal or absolute) |
403 | Caller lacks read-tier role |
404 | Agent not found, or agent not assigned to a crew |
502 | Sidecar IPC call failed |
Git Log
GET /api/v1/agents/{agentId}/git-log
Fetches recent git commits from the agent’s container workspace. Proxied through crewshipd; the agent’s slug is forwarded as agent_slug so the sidecar can scope the log to that agent’s directory inside the shared crew container. (internal/api/proxy.go:312)
Auth: Session or CLI token + workspace membership with at least read-tier role (canRole(role, "read"))
Response: 200 OK — JSON array of commit entries (unwrapped from the IPC layer’s {commits: [...]} envelope). Returns [] when the sidecar response is unparseable or empty.
| Status | Condition |
|---|
403 | Caller lacks read-tier role |
404 | Agent not found, or agent not assigned to a crew |
502 | Sidecar IPC call failed |
Chat Messages
GET /api/v1/chats/{chatId}/messages
Get messages for a specific chat session. Proxied through crewshipd. Supports ?offset= and ?limit= (default 50, max 500).
Auth: Session or CLI token with read-tier role, plus membership of the chat’s workspace. The workspace is resolved from the chat row (no workspace_id query param needed); the caller must appear in workspace_members for that workspace or the call returns 403. (internal/api/proxy.go:258)
Response: 200 OK — proxied message payload. A chat that doesn’t exist yet (new session before its first message) returns { "messages": [] } rather than 404.
| Status | Condition |
|---|
403 | Caller lacks read-tier role, or is not a member of the chat’s workspace |
502 | Sidecar IPC call failed |