This document targets automation / agent clients.
Base URL: http://<host>:<port>/api
Success:
{
"success": true,
"data": {}
}Failure:
{
"success": false,
"error": "...",
"data": {}
}Notes:
dataon failures is optional and may contain structured details.- Some endpoints also return
message.
PilotDeck is local-first.
- If
PM_AGENT_TOKENis NOT set: agent endpoints are open. - If
PM_AGENT_TOKENIS set: sendX-PM-Agent-Token: <token>(orX-PM-Token).
Admin/ops endpoints are separate and require PM_ADMIN_TOKEN via X-PM-Token.
GET: read.POST: create, append, or perform an action.PATCH: partial update (only the fields you send are updated).PUT: full update in HTTP theory; in this codebase it behaves like an update and accepts partial payloads. PreferPATCH.DELETE: delete.
To safely write in multi-agent environments:
GET /projects/<projectId>and captureupdatedAt.- Send your
PATCHincludingifUpdatedAt. - On HTTP
409, re-fetch and retry with a new plan.
Important:
ifUpdatedAtis an exact string match against the currentupdatedAt.- On success, the server always sets a fresh
updatedAt.
This API intentionally separates:
- Project state (stored in
/projects/...) - Audit trail (stored in
/agent/events) - Work-session grouping (stored in
/agent/runs)
Definitions:
run: a container for one agent work session. Use it to group many events/actions under a singlerunId, record final outcome, and attach summary/metrics.event: append-only timeline record. It answers: what happened, when, by whom, on which project/run, and with what details.action: a semantic operation request. In this codebase, actions are not stored as a separate entity; each action writes an event (idempotency key) and may mutate the project.
Relationship:
- One
runcan have manyevents. - One
actionalways produces exactly oneevent(withevent.id == action.id). - A project mutation can be done by:
PATCH /projects/<projectId>(pure state update; no automatic event)POST /agent/actions(state update + automatic event)
Choose the endpoint:
- Use
PATCH /projects/<projectId>when you need full flexibility (any field), and you will also write events explicitly. - Use
POST /agent/actionswhen you want:- a small, safe action vocabulary
- built-in idempotency
- automatic timeline event
The server stores projects as JSON payloads, with a few indexed columns.
Common fields:
id(string, read-only)name(string)status(string):planning|in-progress|paused|completed|cancelledpriority(string):low|medium|high|urgentcategory(string|null)progress(int 0..100)description(string)notes(string)tags(string[])cost(object, at least{ "total": number })revenue(object, at least{ "total": number })createdAt(string, read-only)updatedAt(string, server-managed)
Custom fields:
- You may write additional JSON fields via
PATCH/PUT. They will be persisted, but not necessarily indexed.
Returns server capabilities and enums.
curl -s http://localhost:8689/api/metaQuery params (optional):
statusprioritycategory
curl -s 'http://localhost:8689/api/projects?status=in-progress&priority=high'curl -s http://localhost:8689/api/projects/proj-aaaCreate a project.
Request body (minimum):
{ "name": "My Project" }Meaning:
- Partial update: only fields present in the JSON body are applied.
- Protected fields are ignored:
id,createdAt(andifUpdatedAt). - The server always updates
updatedAt.
Request body:
- Any subset of project fields
- Optional
ifUpdatedAtfor optimistic concurrency
Example: update status and progress
curl -s -X PATCH http://localhost:8689/api/projects/proj-aaa \
-H 'Content-Type: application/json' \
-d '{
"ifUpdatedAt": "2026-01-30T12:34:56.000000",
"status": "in-progress",
"progress": 20
}'Example: add a tag (tags are stored as a full array)
curl -s -X PATCH http://localhost:8689/api/projects/proj-aaa \
-H 'Content-Type: application/json' \
-d '{
"ifUpdatedAt": "2026-01-30T12:34:56.000000",
"tags": ["api", "agent", "pilotdeck"]
}'Success response: HTTP 200
{
"success": true,
"data": { "id": "proj-aaa", "updatedAt": "...", "status": "in-progress" },
"message": "项目更新成功"
}Conflict response: HTTP 409
{
"success": false,
"error": "Conflict: updatedAt mismatch",
"data": { "message": "updatedAt mismatch: expected=... actual=..." }
}Update a project. Prefer PATCH.
Delete a project.
Persist manual ordering.
{ "ids": ["proj-aaa", "proj-bbb"] }Batch patch multiple projects.
{
"ops": [
{
"opId": "op-1",
"id": "proj-aaa",
"ifUpdatedAt": "...",
"patch": { "status": "paused" }
},
{
"opId": "op-2",
"id": "proj-bbb",
"patch": { "progress": 65 }
}
]
}Response includes per-op status (200/404/409/400) without failing the whole request.
Returns aggregated counts and financial totals.
Returns token/cost aggregation for dashboards.
Query params (all optional):
projectIdagentIdworkspacesourcesince(ISO timestamp, inclusive)until(ISO timestamp, inclusive)
Response data includes:
totalsbyDaybyProjectbyAgentbyWorkspacebyModel
These endpoints are used by PilotDeckDesktop to manage reusable Agent behavior.
GET /agent/profiles?enabled=true|falsePOST /agent/profilesGET /agent/profiles/<profileId>PATCH /agent/profiles/<profileId>DELETE /agent/profiles/<profileId>
Suggested profile fields:
{
"id": "agent-desktop-planner",
"name": "Desktop Planner",
"role": "planner",
"description": "Plan-first agent for scoped implementation",
"styleTags": ["cautious", "explainable"],
"skills": ["pilotdeck-skill"],
"outputMode": "concise",
"writebackPolicy": "minimal",
"permissions": {"filesystem": "write", "shell": "restricted"},
"enabled": true,
"meta": {"owner": "team-a"}
}GET /agent/capabilities?enabled=true|falsePOST /agent/capabilitiesGET /agent/capabilities/<capabilityId>PATCH /agent/capabilities/<capabilityId>DELETE /agent/capabilities/<capabilityId>
Suggested capability fields:
{
"id": "cap-default-delivery",
"name": "Default Delivery",
"description": "Balanced delivery with auditable writeback",
"promptPack": "Follow PilotDeck protocol and provide evidence.",
"skillPack": ["pilotdeck-skill"],
"constraints": ["No destructive git commands"],
"enabled": true,
"meta": {"version": "1.0.0"}
}Use these endpoints for token/cost reporting from plugin or desktop clients.
Supports single record or batch (records array).
Single-record body example:
{
"id": "usage-ses-abc123-001",
"ts": "2026-02-06T10:00:00+08:00",
"projectId": "proj-aaa",
"runId": "run-1234",
"agentId": "opencode/sisyphus",
"workspace": "E:/work/PilotDeck",
"sessionId": "ses-abc123",
"source": "opencode-plugin",
"model": "openai/gpt-5.3-codex",
"promptTokens": 1200,
"completionTokens": 560,
"totalTokens": 1760,
"cost": 0.42,
"data": {
"provider": "openai",
"trigger": "session.idle"
}
}Batch body example:
{
"records": [
{
"id": "usage-001",
"projectId": "proj-aaa",
"agentId": "opencode/sisyphus",
"totalTokens": 100,
"promptTokens": 70,
"completionTokens": 30,
"cost": 0.02
},
{
"id": "usage-002",
"projectId": "proj-aaa",
"agentId": "opencode/sisyphus",
"totalTokens": 80,
"promptTokens": 55,
"completionTokens": 25,
"cost": 0.016
}
]
}Idempotency guidance:
- Keep
idstable per usage item (for exampleusage-<sessionId>-<sequence>). - Re-sending the same
idis safe and will not duplicate rows.
Query params (optional):
projectIdagentIdworkspacesourcesinceuntillimit(default 200, max 5000)
- Resolve project identity (from status file):
- Priority 1: Use
project.id→GET /api/projects/<id> - Priority 2: If 404 or missing, search by
project.name→GET /api/projects(client-side filter) - Priority 3: If not found, create →
POST /api/projectswithname: project.name - Update status file with resolved/created
project.id
- Priority 1: Use
- Create run with
POST /agent/runsat task/session start. - Append key timeline events with
POST /agent/events. - Write semantic project changes using
POST /agent/actions(orPATCH /projects/<id>+ event). - Report usage with
POST /agent/usageonsession.idle/ run end. - Finalize run by
PATCH /agent/runs/<runId>withstatus,summary,finishedAt.
Events are append-only and stored in SQLite (data/pm.db).
Use events for:
- auditability (what happened)
- debugging (why did state change)
- handoff (summaries and links)
Event shape (what you can expect on reads):
{
"id": "evt-...",
"ts": "2026-02-03T12:34:56.000000",
"type": "note|action.set_status|...",
"level": "debug|info|warn|error",
"projectId": "proj-...",
"runId": "run-...",
"agentId": "...",
"title": "...",
"message": "...",
"data": {}
}Meaning:
- Append a timeline record.
- If you provide
id, the endpoint becomes idempotent.
When to use:
- You used
PATCH /projects/...and want an audit record. - You want to log a milestone / decision / error.
Request body:
id(optional; if present, used for idempotency)type(optional; defaultnote)level(optional; defaultinfo)projectId/runId/agentId(optional linkage)title/message(optional display fields)data(optional; any JSON-ish value)
Responses:
- HTTP 201: created
- HTTP 200 +
message: "event exists": the sameidalready exists
curl -s -X POST http://localhost:8689/api/agent/events \
-H 'Content-Type: application/json' \
-d '{
"id": "evt-unique-001",
"type": "status-change",
"projectId": "proj-aaa",
"agentId": "opencode/sisyphus",
"message": "Set status to in-progress",
"data": {"from": "planning", "to": "in-progress"}
}'Idempotency:
- The server checks existence by
id. - Re-sending the same
idreturns the existing event instead of inserting a new one.
Meaning:
- List events (best-effort filter).
Query params (optional):
projectId: only events attached to a projectrunId: only events attached to a runagentId: only events from an agenttype: exact match onevent.typesince: ISO timestamp (inclusive)limit: default 200, max 2000
Ordering:
- Returned in ascending time order.
curl -s 'http://localhost:8689/api/agent/events?projectId=proj-aaa&limit=50'Runs are stored in SQLite (data/pm.db). A run groups a series of events/actions.
Think of a run as:
- one agent "work session" (start -> steps -> finish)
- the top-level record you summarize at the end
Run shape (what you can expect on reads):
{
"id": "run-...",
"projectId": "proj-...",
"agentId": "...",
"status": "running|success|failed|cancelled",
"title": "...",
"summary": "...",
"createdAt": "...",
"updatedAt": "...",
"startedAt": "...",
"finishedAt": "...",
"links": [],
"tags": [],
"metrics": {},
"meta": {}
}Meaning:
- Create a new run (or return existing if
idalready exists).
When to use:
- You want to group a batch of actions/events into a single unit of work.
- You want a final status + summary for audit/handoff.
Body fields:
id(optional)status(optional; defaultrunning)projectId,agentId(optional)title,summary(optional)links(optional array)metrics(optional object)tags(optional array)meta(optional object)
Responses:
- HTTP 201: created
- HTTP 200 +
message: "run exists": the sameidalready exists
curl -s -X POST http://localhost:8689/api/agent/runs \
-H 'Content-Type: application/json' \
-d '{
"projectId": "proj-aaa",
"agentId": "opencode/sisyphus",
"title": "Implement agent API",
"tags": ["api", "agent"]
}'Meaning:
- List runs with filtering and pagination.
Query params (optional):
projectIdagentIdstatuslimit(default 50, max 500)offset(default 0)
Meaning:
- Partial update for run metadata.
Protected fields:
id,createdAtare ignored.
Common fields to patch:
status:running|success|failed|cancelledfinishedAt: ISO timestamp (set when finishing)summary: short human-readable summarylinks/metrics/tags/meta
{
"status": "success",
"finishedAt": "2026-01-30T12:34:56.000000",
"summary": "Added /api/projects/batch + agent events"
}Use this endpoint when you want safe, schema-light project mutations with an automatic audit event.
Behavior:
- Executes each action
- Optionally patches the project (unless
recordOnly=true) - Always appends an event with
id == action.id(idempotency key)
Side effects (per action):
- If
recordOnly=falseand the action implies a non-empty patch, the project is updated viaPATCH /projects/<projectId>semantics (includingupdatedAtrefresh + normalization). - An event is always appended with:
type: "action.<actionType>"runId(if provided on request)data.before/data.after(status/priority/progress/tags)data.projectUpdatedAt(the project'supdatedAtafter mutation, or current ifrecordOnly=true)
Request body:
agentId(optional)runId(optional)projectId(optional default for action items)actions(required non-empty array)
Action item fields:
id(optional; recommended for idempotency)projectId(string; required unless top-levelprojectIdis provided)type(required)params(object; optional)recordOnly(bool; optional; when true, no project mutation)ifUpdatedAt(string; optional; optimistic concurrency for project mutation)
Supported type:
set_statusparams:{ "status": "planning|in-progress|paused|completed|cancelled" }set_priorityparams:{ "priority": "low|medium|high|urgent" }set_progressparams:{ "progress": 0..100 }bump_progressparams:{ "delta": -100..100 }(clamped to 0..100)append_noteparams:{ "note": "...", "alsoWriteToProjectNotes": false }add_tagparams:{ "tag": "..." }remove_tagparams:{ "tag": "..." }
Example:
curl -s -X POST http://localhost:8689/api/agent/actions \
-H 'Content-Type: application/json' \
-d '{
"agentId": "opencode/sisyphus",
"runId": "run-1234",
"actions": [
{
"id": "act-unique-001",
"projectId": "proj-aaa",
"type": "set_status",
"params": {"status": "in-progress"},
"ifUpdatedAt": "2026-01-30T12:34:56.000000"
},
{
"id": "act-unique-002",
"projectId": "proj-aaa",
"type": "bump_progress",
"params": {"delta": 10}
},
{
"id": "act-unique-003",
"projectId": "proj-aaa",
"type": "append_note",
"params": {"note": "Implemented /api/agent/actions", "alsoWriteToProjectNotes": true}
}
]
}'Response shape: HTTP 200
{
"success": true,
"data": {
"results": [
{
"id": "act-unique-001",
"success": true,
"status": 200,
"projectId": "proj-aaa",
"changed": true,
"project": {},
"event": {}
}
],
"changed": true,
"lastUpdated": "..."
}
}Per-action failure is reported inside results (the overall response is still HTTP 200):
status: 404when project not foundstatus: 409whenifUpdatedAtconflictsstatus: 400for invalid action input
Idempotency:
- Re-sending the same
idreturns the existing event and current project (withmessage: "action exists").
Notes for clients:
- If you need to update arbitrary project fields (beyond the supported action types), use
PATCH /projects/<projectId>and write your own event. - If you want "add/remove tag" semantics without fetching/merging arrays, prefer actions.
# 1) Discover capabilities
curl -s http://localhost:8689/api/meta
# 2) Pick a project
proj='proj-aaa'
cur=$(curl -s http://localhost:8689/api/projects/$proj)
# 3) Start a run
run=$(curl -s -X POST http://localhost:8689/api/agent/runs -H 'Content-Type: application/json' -d '{"projectId":"proj-aaa","agentId":"my-agent","title":"Work"}')
# 4) Patch project with optimistic concurrency (replace <updatedAt> with the real value you fetched)
curl -s -X PATCH http://localhost:8689/api/projects/$proj \
-H 'Content-Type: application/json' \
-d '{"ifUpdatedAt":"<updatedAt>","progress":30,"status":"in-progress"}'
# 5) Append an audit event
curl -s -X POST http://localhost:8689/api/agent/events \
-H 'Content-Type: application/json' \
-d '{"type":"note","projectId":"proj-aaa","message":"Updated progress to 30%"}'