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.
- Path/query params: none.
- Request body:
Responses
201 Created: the agent was created:
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.
- Path params:
agentId,name(same allowlist as the GET). - Request body:
Responses
200 OK: the file was written (echoes what was stored):
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 agent’s model. It rewrites the stored AgentConfig.primaryModel (the next run reads it), so it needs no Gateway. This route is native-only: an OpenClaw agent changes its model through the OpenClaw config path (PATCH /api/system/openclaw-config { agentModel }), so this route returns 404 for a non-native agent.
- Path params:
agentId. - Request body:
{ model: string }(a native-catalog model id, e.g.claude-sonnet-4-6).
Responses
200 OK: the model was saved:
400 Bad Request: missing agentId segment, or an empty model:
404 Not Found: unknown agent, or a non-native agent (model change via this route is native-only):
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
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.
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