AgentSource syncs each upstream (the OpenClaw Gateway, the in-process native runtime) INTO SQLite. Reads serve SQLite, so the agent list, an agent record, and an agent’s files keep answering even when the Gateway connection is down; a stale flag marks that case. Writes (create), file PUTs, and live session lists delegate to the owning source and return 503 when the source needs a live upstream that is disconnected.
These routes return clawboo-native
AgentRecord shapes, not OpenClaw protocol shapes. The OpenClawAgentSource adapts the Gateway in both directions; a native (clawboo-native) agent is owned by a peer source whose reads and writes are pure SQLite and always work offline. See agent-source internals for the sync discipline and agent model for the registry concept.sourceId); an unknown id falls back to the default (OpenClaw) source so its 404 semantics hold. All POST/PUT routes read a JSON body parsed by express.json({ limit: '2mb' }). Static paths (sync, registry/health, cleanup-ghosts) are registered BEFORE /:agentId so the param does not swallow them.
Routes
The
AgentRecord shape (returned by GET /api/agents, GET /api/agents/:agentId, and inside the create 201):
participantKind: 'human', runtime values beyond openclaw, and tenantId are documented future seams. Today every Gateway-synced record is participantKind: 'agent', runtime: 'openclaw', tenantId: null.GET /api/agents
The primary fleet read. Aggregates listAgents() across EVERY registered source (OpenClaw + native), all SQLite-backed, so the list answers even when the Gateway connection is down. defaultId is the runtime-neutral Boo Zero (resolveBooZero: an explicit override, then the native Boo Zero, then the OpenClaw default), so a native-first install identifies its native Boo Zero. mainKey, stale, and lastSyncedAt stay OpenClaw-derived; stale is true whenever the server-side OpenClaw connection is not connected.
- Path params: none.
- Query params:
- Request body: none.
Responses
200 OK: the merged agent list:
500 Internal Server Error: any failure listing or probing health:
Example
POST /api/agents
Creates an agent through a source. The default source is OpenClaw (Gateway create + agent-file writes + SQLite mirror), which returns 503 when the server-side connection is down. An optional sourceId routes the create to a peer source; clawboo-native writes are pure SQLite and always succeed.
name and every entry in files are injection-scanned before the source write, on the prompt surface: instruction-override phrasing and invisible-Unicode smuggling refuse the create with 422 and write a blocked audit row, while a machine-directed string (a DROP TABLE inside a code reviewer’s worked example) passes and is reported as a review finding. The asymmetry is deliberate: this route is the one every marketplace deploy travels, and first-run onboarding hard-requires deploying a builtin team.
- Path/query params: none.
- Request body:
Responses
201 Created: the agent was created. findings is present only when the scan produced review-level findings:
422 Unprocessable Entity: name or a file carries a blocking prompt-injection finding. Nothing is written:
400 Bad Request: missing or blank name:
400 Bad Request: sourceId does not name a registered source:
503 Service Unavailable: the source needs a live Gateway connection that is down:
500 Internal Server Error: any other failure:
Example
POST /api/agents/sync
Reconciles the OpenClaw source’s upstream into SQLite. With a body it runs the browser-fallback path: the browser, connected through the proxy, pushes its own agents.list result so SQLite warms WITHOUT the server’s own connection. With no body it runs the server-side sync(), which needs the connection and returns 503 when down.
- Path/query params: none.
- Request body (optional: present ⇒ browser-fallback upsert):
agents is an array. The upsert touches Gateway-owned columns only; SQLite-native columns (teamId, personalityConfig, execConfig, avatarSeed, etc.) are preserved.
Responses
200 OK: browser-fallback upsert (body with an agents array):
200 OK: server-side sync (no body):
archived counts agents present in SQLite but gone upstream (their archivedAt is set; the tombstone is reversible and cleared if the agent reappears).
503 Service Unavailable: the server-side sync needs a live connection that is down:
500 Internal Server Error: any other failure:
Example
GET /api/agents/registry/health
Reports the server-side OpenClaw connection state. Always returns 200 (it is a liveness surface, not gated by the connection). Registered before /:agentId so the param does not swallow registry.
- Path/query params: none.
- Request body: none.
Responses
200 OK: the OpenClaw source’s HealthResult:
500 Internal Server Error: a failure reading health:
Example
POST /api/agents/cleanup-ghosts
A one-shot sweep the client invokes after hydrating from the Gateway. The caller passes the IDs of all agents currently alive in the Gateway; the endpoint deletes every local OpenClaw-owned SQLite row NOT in that list, plus its FK-referenced cost / approval rows and its per-agent settings keys. Idempotent. The sweep is scoped to sourceId = 'openclaw'; the live-id list comes from the Gateway, so native (and any future peer-source) agents are never ghosts of it.
- Path params: none.
- Query params:
- Request body:
Responses
400 Bad Request: liveAgentIds is missing or not an array:
400 Bad Request: liveAgentIds is empty and ?allowEmpty=true was not passed:
200 OK: no local rows were stale:
200 OK: stale rows were swept (remaining is the post-sweep agent count, or null):
500 Internal Server Error: a failure during the sweep:
Example
GET /api/agents/:agentId
Returns one agent record, routed to the source that owns the row.
- Path params:
agentId. - Request body: none.
Responses
200 OK: the record:
400 Bad Request: missing agentId segment:
404 Not Found: no agent with that id in its source:
500 Internal Server Error: a failure reading the record:
Example
DELETE /api/agents/:agentId
Archives the agent. The owning source’s archiveAgent deletes the upstream record THEN cleans the local SQLite row plus its FK-referenced children (cost_records, approval_history) and per-agent settings keys. If the server-side Gateway connection is down, it falls back to a SQLite-only cleanup so the local row never rots, disconnect tolerance.
The browser is responsible for the Gateway
agents.delete RPC; this endpoint cleans up clawboo’s local metadata. Without it, deleted agents leave permanent ghost rows that inflate per-team agentCount.- Path params:
agentId. - Request body: none.
Responses
200 OK: archived upstream and locally:
200 OK: the Gateway was disconnected, so only the local rows were cleaned:
400 Bad Request: missing agentId segment:
500 Internal Server Error: a non-disconnect failure during archive:
Example
GET /api/agents/:agentId/files/:name
Reads one agent file through the owning source. The :name segment must be one of the seven canonical agent file names; any other name is rejected before the read.
- Path params:
agentId,name.
:name ∈ AGENTS.md, SOUL.md, IDENTITY.md, USER.md, TOOLS.md, HEARTBEAT.md, MEMORY.md.
- Request body: none.
Responses
200 OK: the file content:
400 Bad Request: missing agentId segment:
400 Bad Request: :name is not a recognized agent file:
503 Service Unavailable: the owning source needs a live Gateway that is down:
500 Internal Server Error: any other failure:
Example
PUT /api/agents/:agentId/files/:name
Writes one agent file through the owning source. Same :name allowlist as the read; the body must carry a string content.
content is injection-scanned on the prompt surface before the write, exactly as POST /api/agents scans its files. A blocking finding refuses the write with 422 plus a blocked audit row, and review findings ride along on the 200.
- Path params:
agentId,name(same allowlist as the GET). - Request body:
Responses
200 OK: the file was written (echoes what was stored). findings is present only when the scan produced review-level findings:
422 Unprocessable Entity: content carries a blocking prompt-injection finding. Nothing is written:
400 Bad Request: missing agentId segment:
400 Bad Request: :name is not a recognized agent file:
400 Bad Request: content is missing or not a string:
503 Service Unavailable: the owning source needs a live Gateway that is down:
500 Internal Server Error: any other failure:
Example
GET /api/agents/:agentId/sessions
Lists the agent’s live sessions. The OpenClaw source delegates LIVE to the Gateway (sessions are runtime-volatile), so this route returns 503 when the connection is down.
- Path params:
agentId. - Request body: none.
Responses
200 OK: the session list:
400 Bad Request: missing agentId segment:
503 Service Unavailable: the source needs a live Gateway that is down:
500 Internal Server Error: any other failure:
Example
PATCH /api/agents/:agentId/model
Changes a native or hermes agent’s model, and optionally its provider. For a native agent it rewrites the stored AgentConfig (the next run reads it), so it needs no Gateway. With a provider in the body, primaryProvider, primaryModel, and envVar move atomically: the model picker offers every provider’s models, and a cross-provider model left on the old provider would post to the wrong endpoint and fail auth. Without a provider, only primaryModel changes; that is the back-compat contract, and also how a custom model id for the agent’s current provider is set. The server has no model-to-provider catalog (custom ids are supported), so it validates the provider name against the known set and trusts the model string; the dashboard sends the pair its catalog derived. A hermes agent routes every model through OpenRouter, so its execConfig is stored as { provider: 'openrouter', model }; an explicit provider other than openrouter is a 400. An OpenClaw agent changes its model through the OpenClaw config path (PATCH /api/system/openclaw-config { agentModel }), so this route returns 404 for other runtimes.
- Path params:
agentId. - Request body:
{ model: string, provider?: string }(modelis a native-catalog or custom model id, e.g.claude-sonnet-4-6;provideris one of the known native providers, e.g.openrouter).
Responses
200 OK: the model (and, when supplied, the provider) was saved. The provider field is echoed back only when the request carried one:
400 Bad Request: missing agentId segment, an empty model, an unknown provider, or a non-openrouter provider on a hermes agent:
404 Not Found: unknown agent, or an agent on a runtime this route does not edit:
500 Internal Server Error: any other failure:
Example
POST /api/agents/:agentId/chat
Drives ONE conversational turn on a clawboo-native agent’s 1:1 personal chat (the Boo Zero personal chat, and any native agent’s chat). The handler persists the user turn under the session key agent:<agentId>:native, then starts the run detached and returns 202 immediately: the reply streams over the SSE route below and is persisted by the native driver, so it is not aborted when the client disconnects. OpenClaw agents keep the Gateway 1:1 path, so this route returns 404 for a non-native agent.
- Path params:
agentId. - Request body:
Responses
202 Accepted: the user turn was persisted and the reply run started:
400 Bad Request: missing agentId segment:
400 Bad Request: missing or blank message:
404 Not Found: the agent’s runtime is not clawboo-native:
500 Internal Server Error: any other failure:
Example
POST /api/agents/:agentId/chat/stop
Aborts the agent’s in-flight 1:1 turn (the composer’s Stop button). A native agent has at most one live 1:1 run, tracked by its session key; with nothing in flight the call is a no-op that still returns 200.
- Path params:
agentId. - Request body: none.
Responses
200 OK: the run was aborted, or there was nothing to abort:
400 Bad Request: missing agentId segment:
500 Internal Server Error: a failure during abort:
Example
GET /api/agents/:agentId/chat/stream
Server-Sent Events live-tail of the agent’s 1:1 session (agent:<agentId>:native). A pure reader: it never drives a turn. Two tiers: committed chat_messages rows polled every 750 ms past an id cursor (durable, carry an id: line, replayed on resume), and ephemeral in-memory bus frames for live tokens and run state (no id: line, never replayed). Resume from a known position via the standard EventSource Last-Event-ID header or the ?since=<id> query param.
- Path params:
agentId. - Query params:
- Request body: none.
This is an SSE route, not request/response. There is no JSON response body; the catalog below describes the wire frames. A missing
agentId ends the response with a bare 400 and no { error } envelope. Each polled batch reads up to 500 rows past the cursor. The stream is cleaned up on req/res close and closes no database handle (the tail reads through the process-wide shared connection).Connection
On open, the handler writesHTTP/1.1 200 with:
: connected comment frame, flushes any rows already past the cursor, and replays the session’s last-published status per agent so a reconnecting client reconciles a badge left stale by a run that ended while no stream was open.
Event catalog
delta and status frames carry no id: line, so they never move the resume cursor: the committed rows are the source of truth, and the deltas only make a turn type out live.
Example
GET /api/agents/:agentId/shell
Reports whether a clawboo-native Boo may ask to run commands. The value is the tools.shell field of that Boo’s stored AgentConfig, and it decides whether the run_command tool is offered to the model at all. It does not decide whether a command may run: run_command puts every single command in front of a person, remembers nothing, and has no allowlist, so a command allowed once asks again the next time.
Absent reads as off. The field is optional and is deliberately not in the native defaults, so a Boo never arrives with a shell already switched on.
This switch is separate from /api/exec-settings, which writes OpenClaw’s Gateway policy. Nothing consults that policy for a native Boo, so the two are not interchangeable and this route refuses any other runtime by name.
- Path params:
agentId. - Request body: none.
Responses
200 OK:
400 Bad Request: the agent is not on the clawboo-native runtime. The response names the runtime it found:
404 Not Found: no agent with that id, or a native agent with no stored config:
500 Internal Server Error: any other failure, redacted:
Example
POST /api/agents/:agentId/shell
Turns the switch on or off. Changing what a Boo may ask to run is a permissions change, so this route sits on the sensitive rate-limit tier, 60 requests a minute per client address, rather than the router-wide general one.
The handler writes the whole tools record back with shell replaced, so the Boo’s other tool flags (memory, tasks, teamchat, and the reserved custom list) survive the write. It then reads the config back and answers with what is stored rather than echoing what you sent.
- Path params:
agentId. - Request body:
The confirmation dialog the dashboard shows before switching this on is browser-side only. This route accepts
{ "enabled": true } directly, and POST /api/agents with sourceId: 'clawboo-native' can carry execConfig.tools.shell at creation time. What holds is that every native Boo created through clawboo’s own screens starts with the shell off, not that one cannot be created with it on.Responses
200 OK: the stored value after the write:
400 Bad Request: enabled is missing or is not a boolean:
400 Bad Request: the agent is not on the clawboo-native runtime:
404 Not Found: no agent with that id, or a native agent with no stored config:
429 Too Many Requests: the sensitive ceiling:
500 Internal Server Error: any other failure, redacted:
Example
Error envelope
Every error response on these routes is the standard{ error: string } envelope, except the SSE route (GET /api/agents/:agentId/chat/stream), whose only pre-stream failure is a bare 400 with no body. The disconnect case is a 503 with the literal { "error": "gateway_disconnected" } on the write/file/session routes; the read routes (GET /api/agents, GET /api/agents/:agentId) keep serving SQLite instead. The two /api/agents/:agentId/shell routes add one field to the envelope on their wrong-runtime 400, runtime, so a caller can say which runtime it actually found.
See also
- Agent model, Boo, Boo Zero, the registry of record
- Agent-source internals, the sync discipline + idempotency rules
- Teams API, assign an agent to a team
- Runtimes API, drive a board task on a runtime;
sourceIdpeer creates - @clawboo/agent-registry,
AgentSource,AgentRecord,AGENT_FILE_NAMES - REST API overview
GET /api/agents/:agentId/workspaces
The task worktrees belonging to this agent’s assigned tasks, newest first. Backs the agent-detail Workspace tab: it picks which worktree to show and drives the honest empty state when the agent has none.
Archived workspaces are omitted. onDisk: false marks a checkout that was paused or reaped, where the row and the branch survive but the files are not there; the tab reports that state rather than an empty tree. The worktree contents themselves come from the /api/board/:taskId/workspace/* routes.
- Path params:
agentId. - Request body: none.
Responses
200 OK:
{ "ok": true, "workspaces": [] }.