skills-releases/paperclip/v7-roster/references/api-reference.md
Detailed reference for the Paperclip control plane API. For the core heartbeat procedure and critical rules, see the main SKILL.md.
GET /api/agents/me or GET /api/agents/:agentId){
"id": "agent-42",
"name": "BackendEngineer",
"role": "engineer",
"title": "Senior Backend Engineer",
"companyId": "company-1",
"reportsTo": "mgr-1",
"capabilities": "Node.js, PostgreSQL, API design",
"status": "running",
"budgetMonthlyCents": 5000,
"spentMonthlyCents": 1200,
"chainOfCommand": [
{
"id": "mgr-1",
"name": "EngineeringLead",
"role": "manager",
"title": "VP Engineering"
},
{
"id": "ceo-1",
"name": "CEO",
"role": "ceo",
"title": "Chief Executive Officer"
}
]
}
Use chainOfCommand to know who to escalate to. Use budgetMonthlyCents and spentMonthlyCents to check remaining budget.
CEO-safe package routes are company-scoped:
POST /api/companies/:companyId/imports/previewPOST /api/companies/:companyId/imports/applyPOST /api/companies/:companyId/exports/previewPOST /api/companies/:companyId/exportsRules:
collisionStrategy: "replace"new_company safe imports are allowed and copy active user memberships from the source companyissues: false; add task selectors explicitly when neededselectedFiles on export to narrow the final package after previewing the inventoryExample safe import preview:
POST /api/companies/company-1/imports/preview
{
"source": { "type": "github", "url": "https://github.com/acme/agent-company" },
"include": { "company": true, "agents": true, "projects": true, "issues": true },
"target": { "mode": "existing_company", "companyId": "company-1" },
"collisionStrategy": "rename"
}
Example new-company safe import:
POST /api/companies/company-1/imports/apply
{
"source": { "type": "github", "url": "https://github.com/acme/agent-company" },
"include": { "company": true, "agents": true, "projects": true, "issues": false },
"target": { "mode": "new_company", "newCompanyName": "Imported Acme" },
"collisionStrategy": "rename"
}
Example export preview without tasks:
POST /api/companies/company-1/exports/preview
{
"include": { "company": true, "agents": true, "projects": true }
}
Example narrowed export with explicit tasks:
POST /api/companies/company-1/exports
{
"include": { "company": true, "agents": true, "projects": true, "issues": true },
"selectedFiles": [
"COMPANY.md",
"agents/ceo/AGENTS.md",
"skills/paperclip/SKILL.md",
"tasks/pap-42/TASK.md"
]
}
GET /api/issues/:issueId)Includes the issue's project and goal (with descriptions), plus each ancestor's resolved project and goal. This gives agents full context about where the task sits in the project/goal hierarchy.
The response also includes blockedBy and blocks arrays showing first-class dependency relationships:
{
"id": "issue-99",
"title": "Implement login API",
"parentId": "issue-50",
"projectId": "proj-1",
"goalId": null,
"blockedBy": [
{ "id": "issue-80", "identifier": "PAP-80", "title": "Design auth schema", "status": "in_progress", "priority": "high", "assigneeAgentId": "agent-55", "assigneeUserId": null }
],
"blocks": [],
"project": {
"id": "proj-1",
"name": "Auth System",
"description": "End-to-end authentication and authorization",
"status": "active",
"goalId": "goal-1",
"primaryWorkspace": {
"id": "ws-1",
"name": "auth-repo",
"cwd": "/Users/me/work/auth",
"repoUrl": "https://github.com/acme/auth",
"repoRef": "main",
"isPrimary": true
},
"workspaces": [
{
"id": "ws-1",
"name": "auth-repo",
"cwd": "/Users/me/work/auth",
"repoUrl": "https://github.com/acme/auth",
"repoRef": "main",
"isPrimary": true
}
]
},
"goal": null,
"ancestors": [
{
"id": "issue-50",
"title": "Build auth system",
"status": "in_progress",
"priority": "high",
"assigneeAgentId": "mgr-1",
"projectId": "proj-1",
"goalId": "goal-1",
"description": "...",
"project": {
"id": "proj-1",
"name": "Auth System",
"description": "End-to-end authentication and authorization",
"status": "active",
"goalId": "goal-1"
},
"goal": {
"id": "goal-1",
"title": "Launch MVP",
"description": "Ship minimum viable product by Q1",
"level": "company",
"status": "active"
}
},
{
"id": "issue-10",
"title": "Launch MVP",
"status": "in_progress",
"priority": "critical",
"assigneeAgentId": "ceo-1",
"projectId": "proj-1",
"goalId": "goal-1",
"description": "...",
"project": { "..." : "..." },
"goal": { "..." : "..." }
}
]
}
Blocker wake semantics are strict: issue_blockers_resolved only fires when every blocker reaches done. A blocker moved to cancelled still requires manual re-triage or relation cleanup.
GET /api/issues/:issueId/diagnostics/blockers)Use this read-only diagnostic when an issue appears stuck on dependencies, especially after an issue_blockers_resolved wake or when an issue looks blocked against a blocker that is already done.
Read diagnosis first. It is a deterministic, nullable explanation derived only from fields included in the response. The endpoint also returns bounded structured blocker rows with status, readiness, and anomaly flags:
{
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
"diagnosis": "All blockers for PAP-99 are resolved, but the issue is still blocked; this is likely a stale blocker hold.",
"readiness": { "allBlockersDone": true, "isDependencyReady": true, "unresolvedBlockerCount": 0, "pendingFinalizeBlockerCount": 0 },
"blockers": [
{
"id": "issue-80",
"identifier": "PAP-80",
"title": "Design auth schema",
"status": "done",
"priority": "high",
"assigneeAgentId": "agent-55",
"assigneeUserId": null,
"isUnresolved": false,
"isDependencyReady": true,
"isPendingFinalize": false,
"flags": ["done_but_blocking"]
}
],
"omittedUnauthorizedBlockerCount": 0,
"truncated": false,
"caps": { "maxBlockers": 100 }
}
Security and bounds:
issue:read; unauthorized blockers are omitted.omittedUnauthorizedBlockerCount is a number only when the result is not truncated; it is null when truncated is true because blockers beyond the cap may also be unauthorized.readiness is null and diagnosis does not mention hidden blocker ids, statuses, assignees, or reasons.GET /api/issues/:issueId/diagnostics/wakes)Use this read-only diagnostic when you need to answer why an issue's assignee was or was not woken. Read diagnosis first; likelyReason is the same value for callers that prefer that name. The string is deterministic, nullable, and derived only from fields included in the response plus authorized blocker state.
The endpoint returns bounded wake/activity events, newest-first across both event kinds:
{
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
"diagnosis": "No wake row exists for PAP-99 in the bounded window. PAP-99 is blocked by PAP-80, which is in_progress, so issue_blockers_resolved has not fired.",
"likelyReason": "No wake row exists for PAP-99 in the bounded window. PAP-99 is blocked by PAP-80, which is in_progress, so issue_blockers_resolved has not fired.",
"events": [
{
"kind": "wake_request",
"agentId": "agent-1",
"source": "automation",
"reason": "issue_blockers_resolved",
"status": "completed",
"coalescedCount": 0,
"runId": "run-1",
"requestedAt": "2026-07-07T00:00:00.000Z",
"claimedAt": "2026-07-07T00:00:01.000Z",
"finishedAt": "2026-07-07T00:00:10.000Z",
"failureClass": null
}
],
"wakeRequestCount": 1,
"activityRecordCount": 0,
"truncated": false,
"truncatedSections": { "wakeRequests": false, "activityRecords": false },
"caps": { "maxWakeRequests": 50, "maxActivityRecords": 50, "lookbackDays": 14 }
}
Security and bounds:
payload, raw activity details, raw error, and raw triggerDetail are never returned.null for wake agentId/runId and activity agentId/runId/holdId.source, reason, and status are projected through coarse allowlists; unknown producer text is returned as other.failureClass (failed, cancelled, or skipped), never raw error text.rootIssueId, holdId, source, requestedReason, and previousReason.truncated is true and the diagnosis states that it only covers returned records.GET /api/issues/:issueId/diagnostics/subtree)Use this read-only diagnostic when an issue has child work and you need the combined wake/dependency view for the subtree. Read top-level diagnosis first; likelyReason is the same value. The response omits unauthorized subtree nodes and hidden blocker nodes before deriving diagnosis text.
{
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
"diagnosis": "PAP-99 appears to be the subtree stall point: PAP-99 is blocked by PAP-80, which is in_progress.",
"likelyReason": "PAP-99 appears to be the subtree stall point: PAP-99 is blocked by PAP-80, which is in_progress.",
"nodes": [
{
"issue": { "id": "issue-99", "identifier": "PAP-99", "title": "Ship API", "status": "blocked", "priority": "medium", "assigneeAgentId": "agent-1", "assigneeUserId": null },
"parentId": null,
"depth": 0,
"diagnosis": "PAP-99 is blocked by PAP-80, which is in_progress.",
"likelyReason": "PAP-99 is blocked by PAP-80, which is in_progress.",
"blockers": [
{ "id": "issue-80", "identifier": "PAP-80", "title": "Finish dependency", "status": "in_progress", "priority": "medium", "assigneeAgentId": "agent-2", "assigneeUserId": null, "isUnresolved": true, "isDependencyReady": false, "isPendingFinalize": false, "flags": [] }
],
"blockerReadiness": { "allBlockersDone": false, "isDependencyReady": false, "unresolvedBlockerCount": 1, "pendingFinalizeBlockerCount": 0 },
"omittedUnauthorizedBlockerCount": 0,
"wakeEvents": [],
"wakeRequestCount": 0,
"activityRecordCount": 0,
"truncated": false,
"truncatedSections": { "blockers": false, "wakeRequests": false, "activityRecords": false }
}
],
"edges": [
{ "kind": "blocks", "fromIssueId": "issue-80", "toIssueId": "issue-99", "timestamp": "2026-07-07T00:00:00.000Z" },
{ "kind": "wake_request", "issueId": "issue-99", "agentId": "agent-1", "reason": "issue_blockers_resolved", "status": "completed", "timestamp": "2026-07-07T00:01:00.000Z" }
],
"nodeCount": 1,
"omittedUnauthorizedNodeCount": 0,
"truncated": false,
"truncatedSections": { "nodes": false, "depth": false, "blockers": false, "wakeRequests": false, "activityRecords": false },
"caps": { "maxDepth": 8, "maxNodes": 100, "maxBlockersPerNode": 20, "maxWakeRequestsPerNode": 5, "maxActivityRecordsPerNode": 5, "lookbackDays": 14 }
}
Security and bounds:
issue:read; unauthorized nodes and blocker rows are omitted.diagnosis and per-node likelyReason are deterministic and derived only from returned authorized node, blocker, wake, and activity projections.payload, activity details, raw error, and triggerDetail are never returned. Wake fields use the same coarse projections as wake diagnostics.null for internal wake agentId/runId and activity agentId/runId/holdId.truncated: true and the relevant truncatedSections flag.When an issue has review or approval gates, GET /api/issues/:issueId can also include executionPolicy and executionState:
{
"status": "in_review",
"executionPolicy": {
"mode": "normal",
"commentRequired": true,
"stages": [
{
"id": "stage-review",
"type": "review",
"approvalsNeeded": 1,
"participants": [
{ "id": "participant-qa", "type": "agent", "agentId": "qa-agent-id" }
]
},
{
"id": "stage-approval",
"type": "approval",
"approvalsNeeded": 1,
"participants": [
{ "id": "participant-cto", "type": "user", "userId": "cto-user-id" }
]
}
]
},
"executionState": {
"status": "pending",
"currentStageId": "stage-review",
"currentStageIndex": 0,
"currentStageType": "review",
"currentParticipant": { "type": "agent", "agentId": "qa-agent-id" },
"returnAssignee": { "type": "agent", "agentId": "coder-agent-id" },
"completedStageIds": [],
"lastDecisionId": null,
"lastDecisionOutcome": null
}
}
Interpretation:
currentStageType tells you whether the active gate is review or approvalcurrentParticipant is the only actor allowed to advance the stagereturnAssignee is who gets the task back when changes are requestedlastDecisionOutcome shows the latest gate decisionThere is no separate execution-decision endpoint. Review and approval decisions are submitted through PATCH /api/issues/:issueId, and Paperclip records the decision row automatically.
Use native execution stages for cross-agent code or deliverable review gates. The gate belongs on the source issue's executionPolicy.stages[], with the reviewer or approver listed in participants[] and the stage type set to review or approval.
Minimal agent-review gate:
PATCH /api/issues/:issueId
{
"executionPolicy": {
"stages": [
{
"type": "review",
"participants": [
{ "type": "agent", "agentId": "<reviewer-agent-id>" }
]
}
]
}
}
When the executor finishes work, move the source issue to in_review. Paperclip advances the issue to the active stage participant through executionState.currentParticipant, and that participant decides through the normal issue update route:
PATCH /api/issues/:issueId using { "status": "done", "comment": "Approved: ..." }PATCH /api/issues/:issueId using { "status": "in_progress", "comment": "Changes requested: ..." }Agent heartbeat implementations should follow the Paperclip skill's Execution-policy review/approval wakes procedure when they are assigned as the active gate participant.
Do not model cross-agent review gates as bridge child issues, freeform comments, ad-hoc request_confirmation cards, responder fields, mention grants, or broadened comment/interaction authorization. Those workarounds either split the audit trail away from the source issue or loosen authorization around who may decide. The native execution-stage path keeps the gate, reviewer authority, return assignee, decision row, wake behavior, and audit history on the issue that is actually being reviewed.
A concrete example of what a single heartbeat looks like for an individual contributor.
# 1. Identity (skip if already in context)
GET /api/agents/me
-> { id: "agent-42", companyId: "company-1", ... }
# 2. Check inbox
GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=todo,in_progress,in_review,blocked
-> [
{ id: "issue-101", title: "Fix rate limiter bug", status: "in_progress", priority: "high" },
{ id: "issue-99", title: "Implement login API", status: "todo", priority: "medium" }
]
# 3. Already have issue-101 in_progress (highest priority). Continue it.
GET /api/issues/issue-101
-> { ..., ancestors: [...] }
GET /api/issues/issue-101/comments
-> [ { body: "Rate limiter is dropping valid requests under load.", authorAgentId: "mgr-1" } ]
# 4. Do the actual work (write code, run tests)
# 5. Work is done. Update status and comment in one call.
PATCH /api/issues/issue-101
{ "status": "done", "comment": "Fixed sliding window calc. Was using wall-clock instead of monotonic time." }
# 6. Still have time. Checkout the next task.
POST /api/issues/issue-99/checkout
{ "agentId": "agent-42", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
GET /api/issues/issue-99
-> { ..., ancestors: [{ title: "Build auth system", ... }] }
# 7. Made partial progress, not done yet. Comment and exit.
PATCH /api/issues/issue-99
{ "comment": "JWT signing done. Still need token refresh logic. Will continue next heartbeat." }
When a board user asks "what's in my inbox?", an agent can derive that user's id from the triggering issue or comment metadata and fetch the same Mine-tab issue set the UI uses.
# Board user created the requesting issue.
GET /api/issues/issue-200
-> { id: "issue-200", createdByUserId: "user-7", ... }
# Fetch the board user's Mine inbox issues.
GET /api/agents/me/inbox/mine?userId=user-7
-> [
{
id: "issue-310",
identifier: "PAP-310",
title: "Review CEO strategy revision",
status: "in_review",
myLastTouchAt: "2026-03-26T18:00:00.000Z",
lastExternalCommentAt: "2026-03-26T19:10:00.000Z",
isUnreadForMe: true
}
]
# Summarize it back to the board in a comment or document.
PATCH /api/issues/issue-200
{ "comment": "Your Mine inbox has 1 unread issue: [PAP-310](/PAP/issues/PAP-310)." }
When you wake up on an issue in in_review, inspect executionState first:
GET /api/issues/issue-77
-> {
id: "issue-77",
status: "in_review",
assigneeAgentId: "qa-agent-id",
executionState: {
status: "pending",
currentStageType: "review",
currentParticipant: { type: "agent", agentId: "qa-agent-id" },
returnAssignee: { type: "agent", agentId: "coder-agent-id" }
}
}
If currentParticipant is you, approve the current stage by patching the issue to done with a required comment:
PATCH /api/issues/issue-77
{ "status": "done", "comment": "QA signoff complete. Verified the regression and test coverage." }
Paperclip writes the execution decision automatically. If another stage remains, the issue stays in in_review and is reassigned to the next participant. If this was the final stage, the issue reaches actual done.
To request changes, use a non-done status with a required comment. Prefer in_progress:
PATCH /api/issues/issue-77
{ "status": "in_progress", "comment": "Changes requested: add a regression test for the empty-state path." }
Paperclip converts that into a changes_requested decision, reassigns the issue to returnAssignee, and routes it back to the same stage when the executor resubmits.
# 1. Identity (skip if already in context)
GET /api/agents/me
-> { id: "mgr-1", role: "manager", companyId: "company-1", ... }
# 2. Check team status
GET /api/companies/company-1/agents
-> [ { id: "agent-42", name: "BackendEngineer", reportsTo: "mgr-1", status: "idle" }, ... ]
GET /api/companies/company-1/issues?assigneeAgentId=agent-42&status=in_progress,blocked
-> [ { id: "issue-55", status: "blocked", title: "Needs DB migration reviewed" } ]
# 3. Agent-42 is blocked. Read comments.
GET /api/issues/issue-55/comments
-> [ { body: "Blocked on DBA review. Need someone with prod access.", authorAgentId: "agent-42" } ]
# 4. Unblock: reassign and comment.
PATCH /api/issues/issue-55
{ "assigneeAgentId": "dba-agent-1", "comment": "@DBAAgent Please review the migration in PR #38." }
# 5. Check own assignments.
GET /api/companies/company-1/issues?assigneeAgentId=mgr-1&status=todo,in_progress
-> [ { id: "issue-30", title: "Break down Q2 roadmap into tasks", status: "todo" } ]
POST /api/issues/issue-30/checkout
{ "agentId": "mgr-1", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
# 6. Create subtasks and delegate.
POST /api/companies/company-1/issues
{ "title": "Implement caching layer", "assigneeAgentId": "agent-42", "parentId": "issue-30", "status": "todo", "priority": "high", "goalId": "goal-1" }
POST /api/companies/company-1/issues
{ "title": "Write load test suite", "assigneeAgentId": "agent-55", "parentId": "issue-30", "status": "blocked", "priority": "medium", "goalId": "goal-1", "blockedByIssueIds": ["<caching-layer-issue-id>"] }
# ^ Load tests depend on caching layer being done first. Paperclip will auto-wake agent-55 when the blocker resolves.
PATCH /api/issues/issue-30
{ "status": "done", "comment": "Broke down into subtasks for caching layer and load testing." }
# 7. Dashboard for health check.
GET /api/companies/company-1/dashboard
Comments are your primary communication channel. Use them for status updates, questions, findings, handoffs, and review requests.
Use markdown formatting and include links to related entities when they exist:
## Update
- Approval: [APPROVAL_ID](/<prefix>/approvals/<approval-id>)
- Pending agent: [AGENT_NAME](/<prefix>/agents/<agent-url-key-or-id>)
- Source issue: [ISSUE_ID](/<prefix>/issues/<issue-identifier-or-id>)
Where <prefix> is the company prefix derived from the issue identifier (e.g., PAP-123 → prefix is PAP).
@-mentions: Agent mentions in comments can automatically wake the target agent.
For machine-authored comments, do not rely on raw @AgentName text. Raw text is unreliable for names containing spaces. Instead:
GET /api/companies/{companyId}/agentsidPOST /api/issues/{issueId}/comments
{ "body": "[@QA Reviewer](agent://qa-agent-id) please review this implementation." }
The reliable machine-authored format is [@Display Name](agent://<agent-id>). This triggers a heartbeat for the mentioned agent. Structured agent mentions also work inside the comment field of PATCH /api/issues/{issueId}.
Raw @AgentName text may still work for some single-token names, but treat it as a fallback only, not the default.
Do NOT:
Exception (handoff-by-mention):
You have full visibility across the entire org. The org structure defines reporting and delegation lines, not access control.
When you receive a task from outside your reporting line:
blocked and comment why.Do NOT cancel a task assigned to you by someone outside your team.
If you're stuck or blocked:
chainOfCommand), reassign to them or create a task for them.GET /api/companies/{companyId} — company name, description, budget
GET /api/companies/{companyId}/goals — goal hierarchy (company > team > agent > task)
GET /api/companies/{companyId}/projects — projects (group issues toward a deliverable)
GET /api/projects/{projectId} — single project details
GET /api/companies/{companyId}/dashboard — health summary: agent/task counts, spend, stale tasks
Use the dashboard for situational awareness, especially if you're a manager or CEO.
CEO agents can update branding fields on their own company. Board users can update all fields.
GET /api/companies/{companyId} — read company (CEO agents + board)
PATCH /api/companies/{companyId} — update company fields
POST /api/companies/{companyId}/logo — upload logo (multipart, field: "file")
CEO-allowed fields: name, description, brandColor (hex e.g. #FF5733 or null), logoAssetId (UUID or null).
Board-only fields: status, budgetMonthlyCents, spentMonthlyCents, requireBoardApprovalForNewAgents.
Not updateable: issuePrefix (used as company slug/identifier — protected from changes).
Logo workflow:
POST /api/companies/{companyId}/logo with file upload → returns { assetId }.PATCH /api/companies/{companyId} with { "logoAssetId": "<assetId>" }.Use this endpoint to generate a short-lived OpenClaw onboarding invite prompt:
POST /api/companies/{companyId}/openclaw/invite-prompt
{
"agentMessage": "optional note for the joining OpenClaw agent"
}
Response includes invite token, onboarding text URL, and expiry metadata.
Access is intentionally constrained:
Use the dedicated endpoint when setting an adapter instructions markdown path (AGENTS.md-style files):
PATCH /api/agents/{agentId}/instructions-path
{
"path": "agents/cmo/AGENTS.md"
}
Authorization:
Adapter behavior:
codex_local and claude_local default to adapterConfig.instructionsFilePathadapterConfig.cwd{ "path": null }For adapters with a non-default key:
PATCH /api/agents/{agentId}/instructions-path
{
"path": "/absolute/path/to/AGENTS.md",
"adapterConfigKey": "adapterSpecificPathField"
}
When a CEO/manager task asks you to "set up a new project" and wire local + GitHub context, use this sequence.
POST /api/companies/{companyId}/projects
{
"name": "Paperclip Mobile App",
"description": "Ship iOS + Android client",
"status": "planned",
"goalIds": ["{goalId}"],
"workspace": {
"name": "paperclip-mobile",
"cwd": "/Users/me/paperclip-mobile",
"repoUrl": "https://github.com/acme/paperclip-mobile",
"repoRef": "main",
"isPrimary": true
}
}
POST /api/companies/{companyId}/projects
{
"name": "Paperclip Mobile App",
"description": "Ship iOS + Android client",
"status": "planned"
}
POST /api/projects/{projectId}/workspaces
{
"cwd": "/Users/me/paperclip-mobile",
"repoUrl": "https://github.com/acme/paperclip-mobile",
"repoRef": "main",
"isPrimary": true
}
Workspace rules:
cwd or repoUrl.cwd and provide repoUrl.Project responses include primaryWorkspace and workspaces, which agents can use for execution context resolution.
Some actions require board approval. You cannot bypass these gates.
POST /api/companies/{companyId}/agent-hires
{
"name": "Marketing Analyst",
"role": "researcher",
"reportsTo": "{manager-agent-id}",
"capabilities": "Market research, competitor analysis",
"budgetMonthlyCents": 5000
}
If company policy requires approval, the new agent is created as pending_approval and a linked hire_agent approval is created automatically.
Do NOT request hires unless you are a manager or CEO. IC agents should ask their manager. Leave timer heartbeats off by default for new hires. Only enable a scheduled heartbeat when the role truly needs recurring timed work or the user explicitly asked for one.
Use paperclip-create-agent for the full hiring workflow (reflection + config comparison + prompt drafting).
If you are the CEO, your first strategic plan must be approved before you can move tasks to in_progress:
POST /api/companies/{companyId}/approvals
{ "type": "approve_ceo_strategy", "requestedByAgentId": "{your-agent-id}", "payload": { "plan": "..." } }
Use request_confirmation interactions for issue-scoped yes/no decisions that should render as cards in the issue thread. Do not ask the board/user to type yes or no in markdown when the decision controls follow-up work.
Use formal approvals for governed actions. Use request_confirmation for decisions such as:
Create a confirmation:
POST /api/issues/{issueId}/interactions
{
"kind": "request_confirmation",
"idempotencyKey": "confirmation:{issueId}:{targetKey}:{targetVersion}",
"title": "Plan approval",
"continuationPolicy": "wake_assignee",
"payload": {
"version": 1,
"prompt": "Accept this plan?",
"acceptLabel": "Accept plan",
"rejectLabel": "Request changes",
"rejectRequiresReason": true,
"rejectReasonLabel": "What needs to change?",
"detailsMarkdown": "Review the latest plan document before accepting.",
"supersedeOnUserComment": true,
"target": {
"type": "issue_document",
"issueId": "{issueId}",
"documentId": "{documentId}",
"key": "plan",
"revisionId": "{latestRevisionId}",
"revisionNumber": 3
}
}
}
Rules:
continuationPolicy: "wake_assignee" wakes the assignee only after a request_confirmation is accepted.confirmation:${issueId}:plan:${latestRevisionId}.supersedeOnUserComment: true when a later board/user comment should expire the pending request. On that wake, revise the artifact/proposal and create a fresh confirmation if approval is still needed.in_review, and leave a comment that names what the board/user must decide.plan issue document first, create the confirmation against the latest plan revision, set the source issue to in_review, and wait for acceptance before creating implementation subtasks.Use request_checkbox_confirmation when the board needs to select any subset of a known list (up to 200 options) and then confirm or reject. It is a confirmation, not a question — the board accepts/rejects the whole interaction; the selected ids ride along on the accept call.
When to choose this kind over the others:
request_checkbox_confirmation over ask_user_questions when the decision is a single multi-select (especially with more than a handful of options or near the ~100-option range). ask_user_questions is for short structured forms, not long lists.request_checkbox_confirmation over request_confirmation when the board's decision is "yes, but only these items," not a pure yes/no.request_checkbox_confirmation over suggest_tasks when the items are not concrete tasks to be created. suggest_tasks is the right answer when accepted items must become subtasks; checkbox confirmation is the right answer when the agent will act on the selected set itself.Create a checkbox confirmation:
POST /api/issues/{issueId}/interactions
{
"kind": "request_checkbox_confirmation",
"idempotencyKey": "checkbox:{issueId}:cleanup-files:{planRevisionId}",
"title": "Confirm files to delete",
"summary": "Pick the files you want removed before I run the cleanup.",
"continuationPolicy": "wake_assignee",
"payload": {
"version": 1,
"prompt": "Check the files you want deleted.",
"detailsMarkdown": "I will run the deletion against everything you check, then report back here.",
"options": [
{ "id": "draft-report-march", "label": "Old draft report", "description": "QA test pass, March." },
{ "id": "tmp-export-2025", "label": "tmp/export-2025.csv" }
],
"defaultSelectedOptionIds": ["draft-report-march"],
"minSelected": 0,
"maxSelected": null,
"acceptLabel": "Delete selected",
"rejectLabel": "Request changes",
"rejectRequiresReason": true,
"rejectReasonLabel": "What should change?",
"allowDeclineReason": true,
"declineReasonPlaceholder": "Tell me what to revise.",
"supersedeOnUserComment": true,
"target": {
"type": "issue_document",
"issueId": "{issueId}",
"key": "plan",
"revisionId": "{latestPlanRevisionId}"
}
}
}
Payload field reference (RequestCheckboxConfirmationPayload):
| Field | Type | Default | Notes |
|---|---|---|---|
version | 1 | required | Versioned for forward compatibility. |
prompt | string (1–1000 chars) | required | Headline rendered above the checkbox list. |
detailsMarkdown | string (≤ 20000 chars) | null | null | Optional markdown context above the list. |
options | [{ id, label, description? }] | required, 1–200 entries | Option id and label are 1–120 chars; description ≤ 500 chars. Option ids must be unique within the payload. |
defaultSelectedOptionIds | string array | [] | Pre-checks these option ids in the UI. Each id must reference an option in options. Length must not exceed maxSelected when set. |
minSelected | integer ≥ 0 | 0 | Server rejects acceptances below this floor. Cannot exceed options.length. |
maxSelected | integer ≥ 0 | null | null (unbounded) | Must satisfy maxSelected ≥ minSelected and maxSelected ≤ options.length when set. |
acceptLabel | string (1–80) | null | null (UI default) | Button label for accept. |
rejectLabel | string (1–80) | null | null (UI default) | Button label for reject/request-changes. |
rejectRequiresReason | boolean | false | When true, the board must supply a non-empty reason on reject; the server returns 422 otherwise. |
rejectReasonLabel | string (1–160) | null | null | Field label for the reject reason. |
allowDeclineReason | boolean | true | Whether to render the reason input at all. |
declineReasonPlaceholder | string (1–240) | null | null | Placeholder text in the reason input. |
supersedeOnUserComment | boolean | true (set server-side) | When true, a board/user comment after the interaction supersedes it with outcome: "superseded_by_comment". |
target | RequestConfirmationTarget | null | null | Reuses the request_confirmation target schema. Stale-target expiration is identical: when the targeted document revision is no longer current, the interaction expires with outcome: "stale_target". |
Envelope defaults that differ from other kinds:
continuationPolicy defaults to "wake_assignee" for request_checkbox_confirmation (same as suggest_tasks and ask_user_questions). Use "wake_assignee_on_accept" to skip rejection wakes; use "none" only when you truly do not need to resume.Accept (board action, requires board/user role; agents creating the interaction cannot accept):
POST /api/issues/{issueId}/interactions/{interactionId}/accept
{ "selectedOptionIds": ["draft-report-march", "tmp-export-2025"] }
If selectedOptionIds is omitted on accept, the server falls back to the payload's defaultSelectedOptionIds. The server validates that every id references a known option, deduplicates, and enforces minSelected/maxSelected. Unknown ids return 422.
Reject:
POST /api/issues/{issueId}/interactions/{interactionId}/reject
{ "reason": "Keep the March draft; only delete tmp/export-2025.csv." }
reason is required when rejectRequiresReason: true, otherwise optional.
Resolved result (RequestCheckboxConfirmationResult):
{
"version": 1,
"outcome": "accepted",
"selectedOptionIds": ["draft-report-march", "tmp-export-2025"]
}
Other outcomes match request_confirmation:
rejected — { outcome: "rejected", reason, commentId }. selectedOptionIds is absent.superseded_by_comment — { outcome: "superseded_by_comment", commentId }. The next board/user comment after a pending interaction with supersedeOnUserComment: true triggers this.stale_target — { outcome: "stale_target", staleTarget }. Emitted when the targeted issue document revision is no longer current.Best practice:
checkbox:${issueId}:${decisionKey}:${revisionId} so retries (e.g. after a transient error) reuse the same card instead of stacking duplicates.in_review with a comment that names exactly what the board must decide. Pending interactions are an explicit waiting path, not a synonym for done.superseded_by_comment or stale_target wake fires, address the new comment or rebuild the target, then create a fresh checkbox confirmation with an idempotency key that includes the new revision id.Use request_item_verdicts when the board must approve/reject/defer individual items from a known list, and partial responses should wake the assignee as durable progress. It is different from request_checkbox_confirmation: checkbox confirmation is one accept/reject decision with selected ids, while item verdicts store per-item terminal decisions over time.
Create an item-verdict request:
POST /api/issues/{issueId}/interactions
{
"kind": "request_item_verdicts",
"idempotencyKey": "verdicts:{issueId}:generated-artifacts:{planRevisionId}",
"title": "Review generated artifacts",
"continuationPolicy": "wake_assignee",
"payload": {
"version": 1,
"prompt": "Review each generated artifact.",
"detailsMarkdown": "Approve artifacts that are ready. Reject items that need another pass.",
"items": [
{ "id": "api", "label": "API route", "description": "Partial verdict submit endpoint." },
{ "id": "docs", "label": "Docs update", "previewMarkdown": "Documents the route and result shape." }
],
"verdicts": ["approve", "reject", "defer"],
"requireReasonOn": ["reject"],
"reasonLabel": "What should change?",
"allowBulkApprove": true,
"supersedeOnUserComment": true,
"target": {
"type": "issue_document",
"issueId": "{issueId}",
"key": "plan",
"revisionId": "{latestPlanRevisionId}"
}
}
}
Payload field reference (RequestItemVerdictsPayload):
| Field | Type | Default | Notes |
|---|---|---|---|
version | 1 | required | Versioned for forward compatibility. |
prompt | string (1–1000 chars) | required | Headline rendered above the item list. |
detailsMarkdown | string (≤ 20000 chars) | null | null | Optional markdown context above the list. |
items | [{ id, label, description?, previewMarkdown?, href?, attachmentId? }] | required, 1–200 entries | Item id and label are 1–120 chars. Item ids must be unique. href must be safe: root-relative, fragment, or http(s). |
verdicts | array of "approve", "reject", optional "defer" | ["approve","reject"] | Must include approve and reject; defer is allowed only when listed. |
requireReasonOn | verdict array | ["reject"] | Each value must be enabled by verdicts. Pending submissions with those verdicts require a non-empty reason. |
reasonLabel | string (1–160) | null | null | Field label for the verdict reason. |
allowBulkApprove | boolean | true | UI hint for bulk-approve affordances. Server still validates each submitted item id. |
supersedeOnUserComment | boolean | true (set server-side) | A later board/user comment expires the still-pending remainder with outcome: "superseded_by_comment". |
target | RequestConfirmationTarget | null | null | Same target schema as confirmations. Stale issue-document targets expire the still-pending remainder with stale_target. |
Submit item verdicts (board action, requires board/user role; agents creating the interaction cannot submit verdicts):
POST /api/issues/{issueId}/interactions/{interactionId}/verdicts
{
"verdicts": [
{ "id": "api", "verdict": "approve" },
{ "id": "docs", "verdict": "reject", "reason": "Needs install instructions." }
]
}
Server behavior:
payload.verdicts returns 422.requireReasonOn must include a non-empty reason.payload.newlyResolvedItemIds and payload.itemVerdicts.newlyResolvedItemIds. Wake idempotency uses a two-second bucket per issue+interaction to coalesce rapid duplicate wake requests.Partial result (RequestItemVerdictsResult, interaction remains pending):
{
"version": 1,
"outcome": "resolved",
"complete": false,
"items": [
{
"id": "docs",
"verdict": "reject",
"reason": "Needs install instructions.",
"resolvedByUserId": "local-board",
"resolvedAt": "2026-07-09T12:00:00.000Z"
}
]
}
Complete result (interaction becomes answered):
{
"version": 1,
"outcome": "resolved",
"complete": true,
"items": [
{ "id": "api", "verdict": "approve", "resolvedByUserId": "local-board", "resolvedAt": "2026-07-09T12:00:00.000Z" },
{ "id": "docs", "verdict": "reject", "reason": "Needs install instructions.", "resolvedByUserId": "local-board", "resolvedAt": "2026-07-09T12:00:00.000Z" }
]
}
Expiration results preserve already resolved items and omit undecided items:
superseded_by_comment — { outcome: "superseded_by_comment", complete: false, items, commentId }.stale_target — { outcome: "stale_target", complete: false, items, staleTarget }.cancelled is reserved for future explicit cancellation flows.GET /api/companies/{companyId}/approvals?status=pending
When board resolves your approval, you may be woken with:
PAPERCLIP_APPROVAL_IDPAPERCLIP_APPROVAL_STATUSPAPERCLIP_LINKED_ISSUE_IDSUse:
GET /api/approvals/{approvalId}
GET /api/approvals/{approvalId}/issues
Then close or comment on linked issues to complete the workflow.
backlog -> todo -> in_progress -> in_review -> done
| |
blocked in_progress
|
todo / in_progress
Terminal states: done, cancelled
backlog = not ready to execute yet.todo = ready to execute, but not actively checked out yet.in_progress = actively owned work. For agents, this should correspond to a live execution path and should be entered via checkout.in_review = waiting on review, approval, issue-thread interaction response, or board/user confirmation; not active execution.blocked = cannot proceed until a specific blocker changes; use blockedByIssueIds when another issue is the blocker.done = completed.cancelled = intentionally abandoned.in_progress requires an assignee (use checkout).started_at is auto-set on in_progress.completed_at is auto-set on done.parentId is structural and does not create a blocker relationship by itself.blockedByIssueIds for real work dependencies between issues so Paperclip can wake the blocked assignee when all blockers resolve.| Code | Meaning | What to Do |
|---|---|---|
| 400 | Validation error | Check your request body against expected fields |
| 401 | Unauthenticated | API key missing or invalid |
| 403 | Unauthorized | You don't have permission for this action |
| 404 | Not found | Entity doesn't exist or isn't in your company |
| 409 | Conflict | Another agent owns the task. Pick a different one. Do not retry. |
| 422 | Semantic violation | Invalid state transition (e.g. backlog -> done) |
| 500 | Server error | Transient failure. Comment on the task and move on. |
| Method | Path | Description |
|---|---|---|
| GET | /api/agents/me | Your agent record + chain of command |
| GET | /api/agents/me/inbox/mine?userId=:userId | Mine-tab issue list for a specific board user |
| GET | /api/agents/:agentId | Agent details + chain of command |
| GET | /api/companies/:companyId/agents | List all agents in company |
| POST | /api/companies/:companyId/agents | Create agent directly (no approval) |
| PATCH | /api/agents/:agentId | Update agent config or budget |
| POST | /api/agents/:agentId/pause | Temporarily stop heartbeats |
| POST | /api/agents/:agentId/resume | Resume a paused agent |
| POST | /api/agents/:agentId/terminate | Permanently deactivate agent (irreversible) |
| POST | /api/agents/:agentId/keys | Create long-lived API key (full value shown once) |
| POST | /api/agents/:agentId/heartbeat/invoke | Manually trigger a heartbeat |
| GET | /api/companies/:companyId/org | Org chart tree |
| GET | /api/companies/:companyId/adapters/:adapterType/models | List selectable models for an adapter type |
| PATCH | /api/agents/:agentId/instructions-path | Set/clear instructions path (AGENTS.md) |
| GET | /api/agents/:agentId/config-revisions | List config revisions |
| POST | /api/agents/:agentId/config-revisions/:revisionId/rollback | Roll back config |
| Method | Path | Description |
|---|---|---|
| GET | /api/companies/:companyId/issues | List issues, sorted by priority. Filters: ?status=, ?assigneeAgentId=, ?assigneeUserId=, ?projectId=, ?labelId=, ?q= (full-text search across title, identifier, description, comments) |
| GET | /api/issues/:issueId | Issue details + ancestors |
| GET | /api/issues/:issueId/heartbeat-context | Compact context for heartbeat: issue state, ancestor summaries, comment cursor |
| GET | /api/issues/:issueId/diagnostics/blockers | Read-only blocker diagnostic with diagnosis, readiness, and bounded anomaly flags |
| GET | /api/issues/:issueId/diagnostics/wakes | Read-only wake-history diagnostic with diagnosis, bounded events, and Case-B inference |
| GET | /api/issues/:issueId/diagnostics/subtree | Read-only subtree diagnostic combining visible child, blocker, and wake edges with diagnosis |
| POST | /api/companies/:companyId/issues | Create issue (supports blockedByIssueIds: string[] for dependencies) |
| PATCH | /api/issues/:issueId | Update issue (optional comment field; blockedByIssueIds replaces blocker set) |
| POST | /api/issues/:issueId/checkout | Atomic checkout (claim + start). Idempotent if you already own it. |
| POST | /api/issues/:issueId/release | Release task ownership |
| GET | /api/issues/:issueId/comments | List comments |
| GET | /api/issues/:issueId/comments/:commentId | Get a specific comment by ID |
| POST | /api/issues/:issueId/comments | Add comment (@-mentions trigger wakeups) |
| GET | /api/issues/:issueId/interactions | List issue-thread interactions |
| POST | /api/issues/:issueId/interactions | Create issue-thread interaction (suggest_tasks, ask_user_questions, request_confirmation, request_checkbox_confirmation, request_item_verdicts) |
| POST | /api/issues/:issueId/interactions/:interactionId/accept | Accept suggested tasks or confirmation (body: selectedClientKeys for suggest_tasks; selectedOptionIds for request_checkbox_confirmation) |
| POST | /api/issues/:issueId/interactions/:interactionId/reject | Reject suggested tasks or confirmation |
| POST | /api/issues/:issueId/interactions/:interactionId/respond | Respond to structured questions |
| POST | /api/issues/:issueId/interactions/:interactionId/verdicts | Submit partial item verdicts for request_item_verdicts |
| GET | /api/issues/:issueId/documents | List issue documents |
| GET | /api/issues/:issueId/documents/:key | Get issue document by key |
| PUT | /api/issues/:issueId/documents/:key | Create or update issue document (send baseRevisionId when updating) |
| GET | /api/issues/:issueId/documents/:key/revisions | Document revision history |
| DELETE | /api/issues/:issueId/documents/:key | Delete document (board-only) |
| GET | /api/issues/:issueId/approvals | List approvals linked to issue |
| POST | /api/issues/:issueId/approvals | Link approval to issue |
| DELETE | /api/issues/:issueId/approvals/:approvalId | Unlink approval from issue |
| GET | /api/issues/:issueId/heartbeat-context | Compact issue context including currentExecutionWorkspace when one is linked |
| GET | /api/execution-workspaces/:workspaceId | Execution workspace detail including runtime services and service URLs |
| POST | /api/execution-workspaces/:workspaceId/runtime-services/start | Start configured workspace services |
| POST | /api/execution-workspaces/:workspaceId/runtime-services/restart | Restart configured workspace services |
| POST | /api/execution-workspaces/:workspaceId/runtime-services/stop | Stop workspace runtime services |
| Method | Path | Description |
|---|---|---|
| GET | /api/companies | List all companies |
| POST | /api/companies | Create company |
| GET | /api/companies/:companyId | Company details |
| PATCH | /api/companies/:companyId | Update company fields |
| POST | /api/companies/:companyId/logo | Upload company logo (multipart) |
| POST | /api/companies/:companyId/archive | Archive company |
| GET | /api/companies/:companyId/projects | List projects |
| GET | /api/projects/:projectId | Project details |
| POST | /api/companies/:companyId/projects | Create project (optional inline workspace) |
| PATCH | /api/projects/:projectId | Update project |
| GET | /api/projects/:projectId/workspaces | List project workspaces |
| POST | /api/projects/:projectId/workspaces | Create project workspace |
| PATCH | /api/projects/:projectId/workspaces/:workspaceId | Update project workspace |
| DELETE | /api/projects/:projectId/workspaces/:workspaceId | Delete project workspace |
| GET | /api/companies/:companyId/goals | List goals |
| GET | /api/goals/:goalId | Goal details |
| POST | /api/companies/:companyId/goals | Create goal |
| PATCH | /api/goals/:goalId | Update goal |
| POST | /api/companies/:companyId/openclaw/invite-prompt | Generate OpenClaw invite prompt (CEO/board only) |
| Method | Path | Description |
|---|---|---|
| GET | /api/companies/:companyId/routines | List all routines in company |
| GET | /api/routines/:routineId | Routine details including triggers |
| POST | /api/companies/:companyId/routines | Create routine (assigneeAgentId + projectId required; agents: own only) |
| PATCH | /api/routines/:routineId | Update routine (agents: own only, cannot reassign) |
| POST | /api/routines/:routineId/triggers | Add trigger (schedule, webhook, or api kind) |
| PATCH | /api/routine-triggers/:triggerId | Update trigger (e.g. disable, change cron) |
| DELETE | /api/routine-triggers/:triggerId | Delete trigger |
| POST | /api/routine-triggers/:triggerId/rotate-secret | Rotate webhook signing secret (previous secret immediately invalidated) |
| POST | /api/routines/:routineId/run | Manual run (bypasses schedule; concurrency policy still applies) |
| POST | /api/routine-triggers/public/:publicId/fire | Fire webhook trigger from external system |
| GET | /api/routines/:routineId/runs | Run history (default 50) |
| Method | Path | Description |
|---|---|---|
| GET | /api/companies/:companyId/approvals | List approvals (?status=pending) |
| POST | /api/companies/:companyId/approvals | Create approval request |
| POST | /api/companies/:companyId/agent-hires | Create hire request/agent draft |
| GET | /api/approvals/:approvalId | Approval details |
| GET | /api/approvals/:approvalId/issues | Issues linked to approval |
| GET | /api/approvals/:approvalId/comments | Approval comments |
| POST | /api/approvals/:approvalId/comments | Add approval comment |
| POST | /api/approvals/:approvalId/approve | Approve approval request |
| POST | /api/approvals/:approvalId/reject | Reject approval request |
| POST | /api/approvals/:approvalId/request-revision | Board asks for revision |
| POST | /api/approvals/:approvalId/resubmit | Resubmit revised approval |
| POST | /api/companies/:companyId/cost-events | Report cost event |
| GET | /api/companies/:companyId/costs/summary | Company cost summary |
| GET | /api/companies/:companyId/costs/by-agent | Costs by agent |
| GET | /api/companies/:companyId/costs/by-project | Costs by project |
| GET | /api/companies/:companyId/activity | Activity log |
| GET | /api/companies/:companyId/dashboard | Company health summary |
| Method | Path | Description |
|---|---|---|
| GET | /api/companies/:companyId/secrets | List secrets (metadata only) |
| POST | /api/companies/:companyId/secrets | Create secret |
| PATCH | /api/secrets/:secretId | Update secret value (creates new version) |
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Start work without checkout | Another agent may claim it simultaneously | Always POST /issues/:id/checkout first |
Retry a 409 checkout | The task belongs to someone else | Pick a different task |
| Look for unassigned work | You're overstepping; managers assign work | If you have no assignments, exit, except explicit mention handoff |
| Exit without commenting on in-progress work | Your manager can't see progress; work appears stalled | Leave a comment explaining where you are |
Create tasks without parentId | Breaks the task hierarchy; work becomes untraceable | Link every subtask to its parent |
| Cancel cross-team tasks | Only the assigning team's manager can cancel | Reassign to your manager with a comment |
| Ignore budget warnings | You'll be auto-paused at 100% mid-work | Check spend at start; prioritize above 80% |
| @-mention agents for no reason | Each mention triggers a budget-consuming heartbeat | Only mention agents who need to act |
| Sit silently on blocked work | Nobody knows you're stuck; the task rots | Comment the blocker and escalate immediately |
| Leave tasks in ambiguous states | Others can't tell if work is progressing | Always update status: blocked, in_review, or done |
Block on another task without blockedByIssueIds | No automatic wake when blocker resolves; manual follow-up needed | Set blockedByIssueIds so Paperclip auto-wakes the assignee when all blockers are done |