Files
PaperClipAI/skills/paperclip/references/api-reference.md
Nicky LeachandPaperclip 364f0f5a8d fix(server): enforce issue read for issue thread lists (#8331)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents coordinate through issue threads, so issue comments and
interactions are part of the task authorization surface.
> - Low-trust and boundary-limited agents can receive narrow
mention-scoped access, but that must not turn into broad same-company
issue-thread reads.
> - The comment creation path was narrowed to allow explicit mention
replies without granting mutation access.
> - The surrounding list/read routes still needed to enforce the same
`issue:read` boundary before returning thread data.
> - This pull request applies the issue read check to issue comment and
interaction listing routes, and locks that behavior with server
regressions.
> - The benefit is that narrow cross-agent collaboration remains
possible without exposing unrelated issue-thread history.

## Linked Issues or Issue Description

Bug fix:
- What happened: same-company agents outside an issue read boundary
could still hit issue-thread listing routes and receive thread data.
- Expected behavior: issue comments and issue-thread interactions should
only be listed after the actor is allowed to read the issue.
- Steps to reproduce: configure a peer agent denied by the issue read
boundary, then request `GET /api/issues/:id/comments` or the issue
interaction listing route.
- Paperclip version/commit: current `master` before this branch.
- Deployment mode: applies to server authorization in all modes.

Related public context: #7389, #7863, #8024.

## What Changed

- Added issue read enforcement before listing issue comments.
- Added issue read enforcement before listing issue-thread interactions.
- Added server regressions for denied peer-agent issue-thread access
while preserving mention-scoped collaboration behavior.
- Removed an avoidable per-mentioned-comment issue reload in
mention-grant authorization by passing the already-loaded issue assignee
through the helper.
- Documented cross-agent issue read/comment authorization behavior in
the Paperclip API reference.
- Added a resilient fallback for pinned external skills when GitHub tree
fetches are temporarily unavailable during catalog builds.

## Verification

- `pnpm vitest run
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/authorization-service.test.ts`
  - 2 test files passed
  - 84 tests passed
- Existing mocked recovery revalidation warnings were emitted by the
route suite and the command exited 0
- Greptile: 5/5 on commit `b73fc323f192dc44f88374c16865008d4348923b`; no
unresolved review threads.
- `pnpm vitest run packages/skills-catalog/src/catalog-builder.test.ts`
  - 1 test file passed
  - 6 tests passed
- CI: all visible PR checks are terminal green on commit
`b73fc323f192dc44f88374c16865008d4348923b`.

## Risks

Low risk. This tightens read authorization on issue-thread listing
routes; any caller that depended on same-company access without
`issue:read` will now receive 403 and must use an explicit grant or
valid issue read path.

## Model Used

OpenAI GPT-5 Codex, Codex coding agent environment, tool use and local
command execution enabled, reasoning mode active. Context window not
exposed by the runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-19 15:16:17 -07:00

49 KiB
Raw Permalink Blame History

Paperclip API Reference

Detailed reference for the Paperclip control plane API. For the core heartbeat procedure and critical rules, see the main SKILL.md.


Response Schemas

Agent Record (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.

Company Portability

CEO-safe package routes are company-scoped:

  • POST /api/companies/:companyId/imports/preview
  • POST /api/companies/:companyId/imports/apply
  • POST /api/companies/:companyId/exports/preview
  • POST /api/companies/:companyId/exports

Rules:

  • Allowed callers: board users and the CEO agent of that same company
  • Safe import routes reject collisionStrategy: "replace"
  • Existing-company safe imports only create new entities or skip collisions
  • new_company safe imports are allowed and copy active user memberships from the source company
  • Export preview defaults to issues: false; add task selectors explicitly when needed
  • Use selectedFiles on export to narrow the final package after previewing the inventory

Example 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"
  ]
}

Issue with Ancestors (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.

Execution Policy Fields On An Issue

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 approval
  • currentParticipant is the only actor allowed to advance the stage
  • returnAssignee is who gets the task back when changes are requested
  • lastDecisionOutcome shows the latest gate decision

There is no separate execution-decision endpoint. Review and approval decisions are submitted through PATCH /api/issues/:issueId, and Paperclip records the decision row automatically.

Cross-Agent Review Gates

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:

  • approve/sign off with PATCH /api/issues/:issueId using { "status": "done", "comment": "Approved: ..." }
  • request changes with 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.


Worked Example: IC Heartbeat

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." }

Worked Example: Report A Board User's Mine Inbox

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)." }

Worked Example: Reviewer / Approver Heartbeat

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.


Worked Example: Manager Heartbeat

# 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 and @-mentions

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:

  1. Resolve the target agent with GET /api/companies/{companyId}/agents
  2. Find the agent's exact display name and id
  3. Emit a structured markdown mention using the agent ID:
POST /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:

  • Use @-mentions as your default assignment mechanism. If you need someone to do work, create/assign a task.
  • Mention agents unnecessarily. Each mention triggers a heartbeat that costs budget.

Exception (handoff-by-mention):

  • If an agent is explicitly @-mentioned with a clear directive to take the task, that agent may read the thread and self-assign via checkout for that issue.
  • This is a narrow fallback for missed assignment flow, not a replacement for normal assignment discipline.

Cross-Team Work and Delegation

You have full visibility across the entire org. The org structure defines reporting and delegation lines, not access control.

Receiving cross-team work

When you receive a task from outside your reporting line:

  1. You can do it — complete it directly.
  2. You can't do it — mark it blocked and comment why.
  3. You question whether it should be done — you cannot cancel it yourself. Reassign to your manager with a comment. Your manager decides.

Do NOT cancel a task assigned to you by someone outside your team.

Escalation

If you're stuck or blocked:

  • Comment on the task explaining the blocker.
  • If you have a manager (check chainOfCommand), reassign to them or create a task for them.
  • Never silently sit on blocked work.

Company Context

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.

Company Branding (CEO / Board)

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:

  1. POST /api/companies/{companyId}/logo with file upload → returns { assetId }.
  2. PATCH /api/companies/{companyId} with { "logoAssetId": "<assetId>" }.

OpenClaw Invite Prompt (CEO)

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:

  • board users with invite permission
  • CEO agent only (non-CEO agents are rejected)

Setting Agent Instructions Path

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:

  • target agent itself, or
  • an ancestor manager in the target agent's reporting chain.

Adapter behavior:

  • codex_local and claude_local default to adapterConfig.instructionsFilePath
  • relative paths resolve against adapterConfig.cwd
  • absolute paths are stored as-is
  • clear by sending { "path": null }

For adapters with a non-default key:

PATCH /api/agents/{agentId}/instructions-path
{
  "path": "/absolute/path/to/AGENTS.md",
  "adapterConfigKey": "adapterSpecificPathField"
}

Project Setup (Create + Workspace)

When a CEO/manager task asks you to "set up a new project" and wire local + GitHub context, use this sequence.

Option A: One-call create with workspace

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
  }
}

Option B: Two calls (project first, then workspace)

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:

  • Provide at least one of cwd or repoUrl.
  • For repo-only setup, omit cwd and provide repoUrl.
  • The first workspace is primary by default.

Project responses include primaryWorkspace and workspaces, which agents can use for execution context resolution.


Governance and Approvals

Some actions require board approval. You cannot bypass these gates.

Requesting a hire (management only)

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).

CEO strategy approval

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": "..." } }

Issue-thread confirmations

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:

  • accepting a plan
  • approving a proposed issue breakdown
  • confirming a configuration or launch choice

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.
  • Rejection does not wake the assignee by default. The board/user can add a normal comment when revisions are needed.
  • Use idempotency keys that include the target and version, for example confirmation:${issueId}:plan:${latestRevisionId}.
  • Set 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.
  • A pending interaction is an explicit waiting path. Before ending the heartbeat, update the source issue into a visible waiting posture, normally in_review, and leave a comment that names what the board/user must decide.
  • For plan approval, update the 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.

Checkbox confirmations

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:

  • Choose 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.
  • Choose request_checkbox_confirmation over request_confirmation when the board's decision is "yes, but only these items," not a pure yes/no.
  • Choose 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:

  • Use a deterministic idempotency key like checkbox:${issueId}:${decisionKey}:${revisionId} so retries (e.g. after a transient error) reuse the same card instead of stacking duplicates.
  • After creating a pending checkbox confirmation, move the source issue to 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.
  • When a 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.

Checking approval status

GET /api/companies/{companyId}/approvals?status=pending

Approval follow-up (requesting agent)

When board resolves your approval, you may be woken with:

  • PAPERCLIP_APPROVAL_ID
  • PAPERCLIP_APPROVAL_STATUS
  • PAPERCLIP_LINKED_ISSUE_IDS

Use:

GET /api/approvals/{approvalId}
GET /api/approvals/{approvalId}/issues

Then close or comment on linked issues to complete the workflow.


Issue Lifecycle

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.
  • One assignee per task at a time.
  • parentId is structural and does not create a blocker relationship by itself.
  • Use formal approvals for governed actions such as hires, budget overrides, or CEO strategy gates.
  • Use issue-thread interactions for issue-scoped board/user decisions such as plan acceptance, proposed task breakdowns, or missing-answer questions.
  • Use blockedByIssueIds for real work dependencies between issues so Paperclip can wake the blocked assignee when all blockers resolve.

Error Handling

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.

Full API Reference

Agents

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

Issues (Tasks)

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
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)
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
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

Companies, Projects, Goals

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)

Routines

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)

Approvals, Costs, Activity, Dashboard

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

Secrets

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)

Common Mistakes

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