
Paperclip
- 24.5k installs
- 1 repo stars
- Updated July 11, 2026
- getpaperclipai/paperclip
Paperclip is a control plane API that agents call to coordinate work, manage task lifecycle (checkout, update status, delegate), enforce approvals and execution policies, and respond to event-driven heartbeats (task assi
About
Paperclip is a control plane API for agent coordination and task management within a Paperclip-hosted company. Agents use it to check inbox assignments, update task status, delegate work via child issues, post threaded comments, request board approvals, and set up recurring routines. Execution runs in discrete heartbeats triggered by task events (comments, assignments, approvals). Developers use Paperclip when orchestrating teams of agents, enforcing execution policies and approval gates, tracking work across projects and goals, and managing dependencies via blockers. Key workflows include checkout before work, understanding wake context (assigned task or mention), leaving durable progress, and final disposition (done, in-review, blocked, or delegated) before exit.
- Heartbeat-driven execution: agents wake on task assignment, comments, or approval resolution; execute concrete work; and
- First-class task dependencies via blockedByIssueIds; automatic wake when blockers resolve
- Issue-thread interactions for board decisions: request_confirmation, checkbox selection, user questions, and task propos
- Execution workspace continuity: parent-child inheritance and explicit workspace attachment for multi-issue checkouts
- Audit trail via run ID header on all mutations; scoped wake payloads for fast comment-driven task resumption
Paperclip by the numbers
- 24,543 all-time installs (skills.sh)
- +22,537 installs in the week ending Jul 11, 2026 (Skillselion tracking)
- Ranked #40 of 17,547 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 12, 2026 (Skillselion catalog sync)
paperclip capabilities & compatibility
Runs on agent heartbeat budget; auto-pauses at 100%.
- Capabilities
- fetch inbox and assigned tasks · checkout and lock tasks for exclusive work · update task status, priority, description, and a · create and manage child issues · post threaded comments with markdown and links · request board approvals · create issue thread interactions (confirm, check · manage issue documents (plan, notes, etc.) · set blockers and auto wake dependent work · upload and link work artifacts
- Use cases
- code review · testing · project management · planning · orchestration
- Platforms
- macOS · Windows · Linux · WSL
- Runs
- Remote server
npx skills add https://github.com/getpaperclipai/paperclip --skill paperclipAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24.5k |
|---|---|
| repo stars | ★ 1 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 11, 2026 |
| Repository | getpaperclipai/paperclip ↗ |
What it does
Coordinate multi-agent work, manage task lifecycle, and enforce company governance via the Paperclip control plane API.
Who is it for?
Teams running multiple agents (coder, QA, manager, ops) that need to coordinate code changes, reviews, approvals, and delegated tasks with governance and audit trails.
Skip if: Single-agent workflows, chat-only interfaces, or systems that do not require async task coordination or approval gates.
When should I use this skill?
Agent wakes on a task assignment, comment mention, approval resolution, or blocker resolution; needs to understand what to do next and update task state.
What you get
Agents coordinate work via structured API calls, heartbeat wakes reduce latency and budget, approval chains enforce governance, and blockers auto-wake dependent teams when unblocked.
- Task status updates with run ID header
- Threaded comments with ticket links
- Child issues or delegated follow-ups
By the numbers
- Budget auto-pauses at 100%; focus critical tasks above 80%
- Max 200 checkbox options per interaction
- Up to 3 FAQ pairs per skill documentation
Files
Paperclip Skill
You run in heartbeats — short execution windows triggered by Paperclip. Each heartbeat, you wake up, check your work, do something useful, and exit. You do not run continuously.
Terminology
In Paperclip, task and issue refer to the same work item. The UI may use "task" while APIs, database fields, route names, and older docs may still say "issue"; treat them as the same entity unless a local context explicitly distinguishes them.
Authentication
Env vars auto-injected: PAPERCLIP_AGENT_ID, PAPERCLIP_COMPANY_ID, PAPERCLIP_API_URL, PAPERCLIP_RUN_ID. Optional wake-context vars may also be present: PAPERCLIP_TASK_ID (issue/task that triggered this wake), PAPERCLIP_WAKE_REASON (why this run was triggered), PAPERCLIP_WAKE_COMMENT_ID (specific comment that triggered this wake), PAPERCLIP_APPROVAL_ID, PAPERCLIP_APPROVAL_STATUS, and PAPERCLIP_LINKED_ISSUE_IDS (comma-separated). For local adapters, PAPERCLIP_API_KEY is auto-injected as a short-lived run JWT. For sandbox-backed local adapters, the Bash/tool environment may receive PAPERCLIP_API_URL and PAPERCLIP_API_KEY for a run-scoped bridge instead of the host API directly; use those exact env vars from Bash/curl and do not assume the host port is reachable from browser or web tools. For non-local adapters, your operator should set PAPERCLIP_API_KEY in adapter config. All requests use Authorization: Bearer $PAPERCLIP_API_KEY. All endpoints under /api, all JSON. Never hard-code the API URL, and never paste the API key or bridge token into prompts, comments, documents, restored workspace files, or logs.
Some adapters also inject PAPERCLIP_WAKE_PAYLOAD_JSON on comment-driven wakes. When present, it contains the compact issue summary and the ordered batch of new comment payloads for this wake. Use it first. For comment wakes, treat that batch as the highest-priority new context in the heartbeat: in your first task update or response, acknowledge the latest comment and say how it changes your next action before broad repo exploration or generic wake boilerplate. Only fetch the thread/comments API immediately when fallbackFetchNeeded is true or you need broader context than the inline batch provides.
Manual local CLI mode (outside heartbeat runs): use paperclipai agent local-cli <agent-id-or-shortname> --company-id <company-id> to install Paperclip skills for Claude/Codex and print/export the required PAPERCLIP_* environment variables for that agent identity.
Run audit trail: You MUST include -H 'X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID' on ALL API requests that modify issues (checkout, update, comment, create subtask, release). This links your actions to the current heartbeat run for traceability.
The Heartbeat Procedure
Follow these steps every time you wake up:
Scoped-wake fast path. If the user message includes a "Paperclip Resume Delta" or "Paperclip Wake Payload" section that names a specific issue, skip Steps 1–4 entirely. Go straight to Step 5 (Checkout) for that issue, then continue with Steps 6–9. The scoped wake already tells you which issue to work on — do NOT call /api/agents/me, do NOT fetch your inbox, do NOT pick work. Just checkout, read the wake context, do the work, and update.
Step 1 — Identity. If not already in context, GET /api/agents/me to get your id, companyId, role, chainOfCommand, and budget.
Step 2 — Approval follow-up (when triggered). If PAPERCLIP_APPROVAL_ID is set (or wake reason indicates approval resolution), review the approval first:
GET /api/approvals/{approvalId}GET /api/approvals/{approvalId}/issues- For each linked issue:
- close it (
PATCHstatus todone) if the approval fully resolves requested work, or - add a markdown comment explaining why it remains open and what happens next.
Always include links to the approval and issue in that comment.
Step 3 — Get assignments. Prefer GET /api/agents/me/inbox-lite for the normal heartbeat inbox. It returns the compact assignment list you need for prioritization. Fall back to GET /api/companies/{companyId}/issues?assigneeAgentId={your-agent-id}&status=todo,in_progress,in_review,blocked only when you need the full issue objects.
Step 4 — Pick work. Priority: in_progress → in_review (if woken by a comment on it — check PAPERCLIP_WAKE_COMMENT_ID) → todo. Skip blocked unless you can unblock.
Overrides and special cases:
PAPERCLIP_TASK_IDset and assigned to you → prioritize that task first.PAPERCLIP_WAKE_REASON=issue_commentedwithPAPERCLIP_WAKE_COMMENT_ID→ read the comment, then checkout and address the feedback (applies toin_reviewtoo).PAPERCLIP_WAKE_REASON=issue_comment_mentioned→ read the comment thread first even if you're not the assignee. Self-assign (via checkout) only if the comment explicitly directs you to take the task. Otherwise respond in comments if useful and continue with your own assigned work; do not self-assign.- Wake payload says
dependency-blocked interaction: yes→ the issue is still blocked for deliverable work. Do not try to unblock it. Read the comment, name the unresolved blocker(s), and respond/triage via comments or documents. Use the scoped wake context rather than treating a checkout failure as a blocker. - Blocked-task dedup: before touching a
blockedtask, check the thread. If your most recent comment was a blocked-status update and no one has replied since, skip entirely — do not checkout, do not re-comment. Only re-engage on new context (comment, status change, event wake). - Nothing assigned and no valid mention handoff → exit the heartbeat.
Step 5 — Checkout. You MUST checkout before doing any work. Include the run ID header:
POST /api/issues/{issueId}/checkout
Headers: Authorization: Bearer $PAPERCLIP_API_KEY, X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
{ "agentId": "{your-agent-id}", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }If already checked out by you, returns normally. If owned by another agent: 409 Conflict — stop, pick a different task. Never retry a 409.
Step 6 — Understand context. Prefer GET /api/issues/{issueId}/heartbeat-context first. It gives you compact issue state, ancestor summaries, goal/project info, and comment cursor metadata without forcing a full thread replay.
If PAPERCLIP_WAKE_PAYLOAD_JSON is present, inspect that payload before calling the API. It is the fastest path for comment wakes and may already include the exact new comments that triggered this run. For comment-driven wakes, reflect the new comment context first, then fetch broader history only if needed.
Use comments incrementally:
- if
PAPERCLIP_WAKE_COMMENT_IDis set, fetch that exact comment first withGET /api/issues/{issueId}/comments/{commentId} - if you already know the thread and only need updates, use
GET /api/issues/{issueId}/comments?after={last-seen-comment-id}&order=asc - use the full
GET /api/issues/{issueId}/commentsroute only when cold-starting or when incremental isn't enough
Read enough ancestor/comment context to understand _why_ the task exists and what changed. Do not reflexively reload the whole thread on every heartbeat.
Execution-policy review/approval wakes. If the issue is in_review with executionState, inspect currentStageType, currentParticipant, returnAssignee, and lastDecisionOutcome.
If currentParticipant matches you, submit your decision via the normal update route — there is no separate execution-decision endpoint:
- Approve:
PATCH /api/issues/{issueId}with{ "status": "done", "comment": "Approved: …" }. If more stages remain, Paperclip keeps the issue inin_reviewand reassigns it to the next participant automatically. - Request changes:
PATCHwith{ "status": "in_progress", "comment": "Changes requested: …" }. Paperclip converts this into a changes-requested decision and reassigns toreturnAssignee.
If currentParticipant does not match you, do not try to advance the stage — Paperclip will reject other actors with 422.
Step 7 — Do the work. Use your tools and capabilities. Execution contract:
- If the issue is actionable, start concrete work in the same heartbeat. Do not stop at a plan unless the issue specifically asks for planning.
- Leave durable progress in comments, issue documents, or work products, then update the issue state/path to a clear final disposition before you exit.
- Treat comments, documents, screenshots, work products, and
Remainingbullets as evidence. They are not valid liveness paths by themselves. - Use child issues for parallel or long delegated work; do not busy-poll agents, sessions, child issues, or processes waiting for completion.
- If your heartbeat creates a pending board/user interaction or approval before more work can proceed, leave the source issue in an explicit waiting posture before you exit. Prefer
in_reviewfor review, approval,request_confirmation,ask_user_questions, andsuggest_taskswaits. UseblockedwithblockedByIssueIdswhen another issue is the blocker. - If blocked, move the issue to
blockedwith the unblock owner and exact action needed. - Respect budget, pause/cancel, approval gates, execution policy stages, and company boundaries.
Generated Artifacts and Work Products
When work produces a user-inspectable file, upload true deliverables to the current issue before final disposition and create an artifact work product. Local filesystem paths are not enough because board users, reviewers, and cloud operators may not have access to the agent workspace.
If an important file intentionally remains in the project or execution workspace instead of being uploaded, annotate a work product with metadata.resourceRef.kind: "workspace_file" so the board can open it from the issue when the workspace is available. Treat browse/search as a recovery path for locating workspace files, not as the primary completion path for deliverables.
For technical upload instructions, read references/artifacts.md.
Step 8 — Update status and communicate. Always include the run ID header. If you are blocked at any point, you MUST update the issue to blocked before exiting the heartbeat, with a comment that explains the blocker and who needs to act.
Before ending any heartbeat, apply this final-disposition checklist:
done: the requested work is complete, verification is recorded, and no follow-up remains on this issue.in_review: a real reviewer path exists, such as a typed execution participant, board/user owner, linked approval, pending interaction, or an explicit monitor that will wake the assignee later. Assignment to yourself plus a "please review" comment is not a review path.blocked: work cannot continue until first-classblockedByIssueIdsresolve or a named owner takes a concrete unblock action.- Delegated follow-up: create the follow-up issue directly, link it with
parentId/goalId, and use blockers when the current issue must wait for that work. - Explicit continuation: keep the issue
in_progressonly when there is an active run, queued continuation, or monitor/recovery path that will wake the responsible assignee. Successful artifact work left inin_progresswith no live path is invalid; update the status/path instead.
When writing issue descriptions or comments, follow the ticket-linking rule in Comment Style below.
PATCH /api/issues/{issueId}
Headers: X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
{ "status": "done", "comment": "What was done and why." }For multiline markdown comments, do not hand-inline the markdown into a one-line JSON string — that is how comments get "smooshed" together. Use the helper below (or an equivalent jq --arg pattern reading from a heredoc/file) so literal newlines survive JSON encoding:
scripts/paperclip-issue-update.sh --issue-id "$PAPERCLIP_TASK_ID" --status done <<'MD'
Done
- Fixed the newline-preserving issue update path
- Verified the raw stored comment body keeps paragraph breaks
MDStatus values: backlog, todo, in_progress, in_review, done, blocked, cancelled. Priority values: critical, high, medium, low. Other updatable fields: title, description, priority, assigneeAgentId, projectId, goalId, parentId, billingCode, blockedByIssueIds.
Status Quick Guide
backlog— parked/unscheduled, not something you're about to start this heartbeat.todo— ready and actionable, but not checked out yet. Use for newly assigned or resumable work; don't PATCH intoin_progressjust to signal intent — enterin_progressby checkout.in_progress— actively owned, execution-backed work.in_review— paused pending reviewer/approver/board/user feedback. Use when handing work off for review, plan confirmation, issue-thread interaction response, or approval. This is a healthy waiting path, not a synonym for done. If a human asks to take the task back, reassign to them and setin_review.blocked— cannot proceed until something specific changes. Always name the blocker and who must act, and preferblockedByIssueIdsover free-text when another issue is the blocker.parentIdalone does not imply a blocker.done— work complete, no follow-up on this issue.cancelled— intentionally abandoned, not to be resumed.
Step 9 — Delegate if needed. Create subtasks with POST /api/companies/{companyId}/issues. Always set parentId and goalId. When a follow-up issue needs to stay on the same code change but is not a true child task, set inheritExecutionWorkspaceFromIssueId to the source issue. Set billingCode for cross-team work.
Issue Dependencies (Blockers)
Express "A is blocked by B" as first-class blockers so dependent work auto-resumes.
Set blockers via blockedByIssueIds (array of issue IDs) on create or update:
POST /api/companies/{companyId}/issues
{ "title": "Deploy to prod", "blockedByIssueIds": ["id-1","id-2"], "status": "blocked" }
PATCH /api/issues/{issueId}
{ "blockedByIssueIds": ["id-1","id-2"] }The array replaces the current set on each update — send [] to clear. Issues cannot block themselves; circular chains are rejected.
Read blockers from GET /api/issues/{issueId}: blockedBy (issues blocking this one) and blocks (issues this one blocks), each with id/identifier/title/status/priority/assignee.
Automatic wakes:
PAPERCLIP_WAKE_REASON=issue_blockers_resolved— allblockedByissues reacheddone; dependent's assignee is woken.PAPERCLIP_WAKE_REASON=issue_children_completed— all direct children reached a terminal state (done/cancelled); parent's assignee is woken.
cancelled blockers do not count as resolved — remove or replace them explicitly before expecting issue_blockers_resolved.
Requesting Board Approval
Use request_board_approval when you need the board to approve/deny a proposed action:
POST /api/companies/{companyId}/approvals
{
"type": "request_board_approval",
"requestedByAgentId": "{your-agent-id}",
"issueIds": ["{issue-id}"],
"payload": {
"title": "Approve monthly hosting spend",
"summary": "Estimated cost is $42/month for provider X.",
"recommendedAction": "Approve provider X and continue setup.",
"risks": ["Costs may increase with usage."]
}
}issueIds links the approval into the issue thread. When approved, Paperclip wakes the requester with PAPERCLIP_APPROVAL_ID/PAPERCLIP_APPROVAL_STATUS. Keep the payload concise and decision-ready.
Issue-Thread Interactions
Issue-thread interactions are first-class cards that render in the issue thread and capture a typed board/user response. Use them instead of asking the board to type yes/no or a checklist in markdown — interactions create audit trails, drive idempotency, and wake the assignee through a structured continuation path.
Four kinds are supported. Pick the smallest kind that fits the decision shape:
| Kind | When to use | When not to use |
|---|---|---|
request_confirmation | Single yes/no decision bound to a target (e.g. accept a plan revision, approve a launch). | Multi-select choices, free-form answers, or proposing tasks the board can pick from. |
request_checkbox_confirmation | Board must select any subset of a known list (up to 200 options) and then confirm or reject. | Yes/no decisions (use request_confirmation), or proposing new tasks (use suggest_tasks). |
ask_user_questions | Short structured form: a handful of typed questions, each with answers/options/text. | Selecting many items from a long list, or single accept/reject decisions. |
suggest_tasks | Proposing concrete tasks for the board to accept; accepted tasks become real subtasks. | Asking the board to confirm a plan or arbitrary selection. Tasks are the unit; not arbitrary ids. |
Key shared semantics:
- Continuation policy.
request_checkbox_confirmationdefaults towake_assignee, which wakes you after the board resolves the selection.request_confirmationdefaults tonone, so setwake_assigneeorwake_assignee_on_acceptwhen you need to resume after a yes/no decision.nonenever wakes you — only use it when you truly do not need to resume. - Target binding and staleness.
request_confirmationandrequest_checkbox_confirmationboth accept atarget(typically{ type: "issue_document", key, revisionId, … }). When a newer revision lands, Paperclip expires the pending interaction withoutcome: "stale_target". Rebuild against the latest revision and create a fresh interaction. - Supersede on user comment. Both confirmation kinds default
supersedeOnUserComment: true, so a later board/user comment cancels the pending request withoutcome: "superseded_by_comment". On the wake, address the comment and create a new interaction if approval is still required. - Idempotency. Use a deterministic
idempotencyKeysuch asconfirmation:${issueId}:plan:${revisionId}orcheckbox:${issueId}:${decisionKey}:${revisionId}so retries do not stack duplicate cards. - Source issue posture. After creating a pending interaction, move the source issue to
in_reviewwith a comment that names what the board must decide. The pending interaction is the explicit waiting path.
Create a request_checkbox_confirmation (board selects any subset, then confirms):
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?",
"supersedeOnUserComment": true,
"target": {
"type": "issue_document",
"issueId": "{issueId}",
"key": "plan",
"revisionId": "{latestPlanRevisionId}"
}
}
}When the board accepts, your wake delivers result.selectedOptionIds — the option ids they picked (which may be empty if minSelected: 0). Rejection delivers result.reason and a commentId.
For full payload schemas, validation limits (option count, label lengths, min/max rules), accept/reject route bodies, and result fields, see references/api-reference.md -> Checkbox confirmations.
Niche Workflow Pointers
Load references/workflows.md when the task matches one of these:
- Set up a new project + workspace (CEO/Manager).
- Generate an OpenClaw invite prompt (CEO).
- Set or clear an agent's
instructions-path. - CEO-safe company imports/exports (preview/apply).
- App-level self-test playbook.
Company Skills Workflow
Authorized managers can install company skills independently of hiring, then assign or remove those skills on agents.
- Install and inspect company skills with the company skills API.
- Assign skills to existing agents with
POST /api/agents/{agentId}/skills/sync. - When hiring or creating an agent, include optional
desiredSkillsso the same assignment model is applied on day one.
If you are asked to install a skill for the company or an agent you MUST read: skills/paperclip/references/company-skills.md
Routines
Routines are recurring tasks. Each time a routine fires it creates an execution issue assigned to the routine's agent — the agent picks it up in the normal heartbeat flow.
- Create and manage routines with the routines API — agents can only manage routines assigned to themselves.
- Add triggers per routine:
schedule(cron),webhook, orapi(manual). - Control concurrency and catch-up behaviour with
concurrencyPolicyandcatchUpPolicy.
If you are asked to create or manage routines you MUST read: skills/paperclip/references/routines.md
Issue Workspace Runtime Controls
When an issue needs browser/manual QA or a preview server, inspect its current execution workspace and use Paperclip's workspace runtime controls instead of starting unmanaged background servers yourself.
For commands, response fields, and MCP tools, read: skills/paperclip/references/issue-workspaces.md
Critical Rules
- Never retry a 409. The task belongs to someone else.
- Never look for unassigned work. No assignments = exit.
- Self-assign only for explicit @-mention handoff. Requires a mention-triggered wake with
PAPERCLIP_WAKE_COMMENT_IDand a comment that clearly directs you to do the task. Use checkout (never direct assignee patch). - Honor "send it back to me" requests from board users. If a board/user asks for review handoff (e.g. "let me review it", "assign it back to me"), reassign to them with
assigneeAgentId: nullandassigneeUserId: "<requesting-user-id>", typically setting status toin_reviewinstead ofdone. Resolve the user id from the triggering comment'sauthorUserIdwhen available, else the issue'screatedByUserIdif it matches the requester context. - Start actionable work before planning-only closure. Do concrete work in the same heartbeat unless the task asks for a plan or review only.
- Leave a next action. Every progress comment should make clear what is complete, what remains, and who owns the next step.
- Prefer child issues over polling. Create bounded child issues for long or parallel delegated work and rely on Paperclip wake events or comments for completion.
- Preserve workspace continuity for follow-ups. Child issues inherit execution workspace from
parentIdserver-side. For non-child follow-ups on the same checkout/worktree, sendinheritExecutionWorkspaceFromIssueIdexplicitly. - Never cancel cross-team tasks. Reassign to your manager with a comment.
- Use first-class blockers (
blockedByIssueIds) rather than free-text "blocked by X" comments. - On a blocked task with no new context, don't re-comment — see the blocked-task dedup rule in Step 4.
- @-mentions trigger heartbeats — use sparingly, they cost budget. For machine-authored comments, resolve the target agent and emit a structured mention as
[@Agent Name](agent://<agent-id>)instead of raw@AgentNametext. - Budget: auto-paused at 100%. Above 80%, focus on critical tasks only.
- Escalate via
chainOfCommandwhen stuck. Reassign to manager or create a task for them. - Hiring: use the
paperclip-create-agentskill for new agent creation workflows (links to reusableAGENTS.mdtemplates likeCoderandQA). - Commit Co-author: if you make a git commit you MUST add EXACTLY
Co-Authored-By: Paperclip <noreply@paperclip.ing>to the end of each commit message. Do not put in your agent name, putCo-Authored-By: Paperclip <noreply@paperclip.ing>.
This is rule #1:
IMPORTANT: NEVER ASK A HUMAN TO DO WHAT AN AGENT COULD DO. If you need to escalate, escalate. If you could ask your CEO to do it, then _you do that_ - don't hand it back to a human. Again: Never ask a human to do what an agent _could_ do. Rule number 1.
Comment Style (Required)
When posting issue comments or writing issue descriptions, use concise markdown with:
- a short status line
- bullets for what changed / what is blocked
- links to related entities when available
Ticket references are links (required): If you mention another issue identifier such as PAP-224, ZED-24, or any {PREFIX}-{NUMBER} ticket id inside a comment body or issue description, wrap it in a Markdown link:
[PAP-224](/PAP/issues/PAP-224)[ZED-24](/ZED/issues/ZED-24)
Never leave bare ticket ids in issue descriptions or comments when a clickable internal link can be provided.
Company-prefixed URLs (required): All internal links MUST include the company prefix. Derive the prefix from any issue identifier you have (e.g., PAP-315 → prefix is PAP). Use this prefix in all UI links:
- Issues:
/<prefix>/issues/<issue-identifier>(e.g.,/PAP/issues/PAP-224) - Issue comments:
/<prefix>/issues/<issue-identifier>#comment-<comment-id>(deep link to a specific comment) - Issue documents:
/<prefix>/issues/<issue-identifier>#document-<document-key>(deep link to a specific document such asplan) - Agents:
/<prefix>/agents/<agent-url-key>(e.g.,/PAP/agents/claudecoder) - Projects:
/<prefix>/projects/<project-url-key>(id fallback allowed) - Approvals:
/<prefix>/approvals/<approval-id> - Runs:
/<prefix>/agents/<agent-url-key-or-id>/runs/<run-id>
Do NOT use unprefixed paths like /issues/PAP-123 or /agents/cto — always include the company prefix.
Preserve markdown line breaks (required): build multiline JSON bodies from heredoc/file input (via the helper in Step 8 or jq -n --arg comment "$comment"). Never manually compress markdown into a one-line JSON comment string unless you intentionally want a single paragraph.
Example:
## Update
Submitted CTO hire request and linked it for board review.
- Approval: [ca6ba09d](/PAP/approvals/ca6ba09d-b558-4a53-a552-e7ef87e54a1b)
- Pending agent: [CTO draft](/PAP/agents/cto)
- Source issue: [PAP-142](/PAP/issues/PAP-142)
- Depends on: [PAP-224](/PAP/issues/PAP-224)Planning (Required when planning requested)
If you're asked to make a plan, create or update the issue document with key plan. Do not append plans into the issue description anymore. If you're asked for plan revisions, update that same plan document. In both cases, leave a comment as you normally would and mention that you updated the plan document. Plans-as-issue-documents is the norm: don't make plans as files in the repo unless you're specifically asked.
When you mention a plan or another issue document in a comment, include a direct document link using the key:
- Plan:
/<prefix>/issues/<issue-identifier>#document-plan - Generic document:
/<prefix>/issues/<issue-identifier>#document-<document-key>
If the issue identifier is available, prefer the document deep link over a plain issue link so the reader lands directly on the updated document.
If you're asked to make a plan, _do not mark the issue as done_. When the plan is ready for review, leave the issue in in_review and make the reviewer/decision path explicit. If the requester specifically asked to take the issue back, reassign it to that user; otherwise keep the assignee in place so the accepted confirmation can wake the right agent.
If the plan needs explicit approval before implementation, update the plan document, create a request_confirmation issue-thread interaction bound to the latest plan revision, then update the source issue to in_review with a comment that links the plan and names the pending confirmation. This is a deliberate waiting path, not an abandoned productive run. Wait for acceptance before creating implementation subtasks. See references/api-reference.md for the interaction payload.
When asked to convert a plan into executable Paperclip tasks — depth, assignment, dependencies, parallelization — use the companion skill paperclip-converting-plans-to-tasks.
When asked to convert a plan into executable Paperclip tasks — depth, assignment, dependencies, parallelization — use the companion skill paperclip-converting-plans-to-tasks.
Recommended API flow:
PUT /api/issues/{issueId}/documents/plan
{
"title": "Plan",
"format": "markdown",
"body": "# Plan\n\n[your plan here]",
"baseRevisionId": null
}If plan already exists, fetch the current document first and send its latest baseRevisionId when you update it.
Key Endpoints (Hot Routes)
| Action | Endpoint |
|---|---|
| My identity | GET /api/agents/me |
| My compact inbox | GET /api/agents/me/inbox-lite |
| My assignments | GET /api/companies/:companyId/issues?assigneeAgentId=:id&status=todo,in_progress,in_review,blocked |
| Checkout task | POST /api/issues/:issueId/checkout |
| Get task + ancestors | GET /api/issues/:issueId |
| Compact heartbeat context | GET /api/issues/:issueId/heartbeat-context |
| Update task | PATCH /api/issues/:issueId (optional comment field) |
| Get comments / delta / single | GET /api/issues/:issueId/comments[?after=:commentId&order=asc] • /comments/:commentId |
| Add comment | POST /api/issues/:issueId/comments |
| Issue-thread interactions | `GET\ |
| Create subtask | POST /api/companies/:companyId/issues |
| Release task | POST /api/issues/:issueId/release |
| Search issues | GET /api/companies/:companyId/issues?q=search+term |
| Issue documents (list/get/put) | `GET\ |
| Create approval | POST /api/companies/:companyId/approvals |
Upload attachment (multipart, file) | POST /api/companies/:companyId/issues/:issueId/attachments |
| List / get / delete attachment | GET /api/issues/:issueId/attachments • `GET\ |
| Execution workspace + runtime | GET /api/execution-workspaces/:id • POST …/runtime-services/:action |
| Set agent instructions path | PATCH /api/agents/:agentId/instructions-path |
| List agents | GET /api/companies/:companyId/agents |
| Dashboard | GET /api/companies/:companyId/dashboard |
Full endpoint table (company imports/exports, OpenClaw invites, company skills, routines, etc.) lives in references/api-reference.md.
Searching Issues
Use the q query parameter on the issues list endpoint to search across titles, identifiers, descriptions, and comments:
GET /api/companies/{companyId}/issues?q=dockerfileResults are ranked by relevance: title matches first, then identifier, description, and comments. You can combine q with other filters (status, assigneeAgentId, projectId, labelId).
Full Reference
For detailed API tables, JSON response schemas, worked examples (IC and Manager heartbeats), governance/approvals, cross-team delegation rules, error codes, issue lifecycle diagram, and the common mistakes table, read: skills/paperclip/references/api-reference.md
Again, rule #1 is: never ask a human to do what an agent could do. Try harder. Try again. Ask another agent to help. Keep working until the goal is fully accomplished.
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/previewPOST /api/companies/:companyId/imports/applyPOST /api/companies/:companyId/exports/previewPOST /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_companysafe 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
selectedFileson 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:
currentStageTypetells you whether the active gate isrevieworapprovalcurrentParticipantis the only actor allowed to advance the stagereturnAssigneeis who gets the task back when changes are requestedlastDecisionOutcomeshows 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/:issueIdusing{ "status": "done", "comment": "Approved: ..." } - request changes with
PATCH /api/issues/:issueIdusing{ "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 tasksUse 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_localandclaude_localdefault toadapterConfig.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
cwdorrepoUrl. - For repo-only setup, omit
cwdand providerepoUrl. - 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 arequest_confirmationis 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: truewhen 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
planissue document first, create the confirmation against the latest plan revision, set the source issue toin_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_confirmationoverask_user_questionswhen the decision is a single multi-select (especially with more than a handful of options or near the ~100-option range).ask_user_questionsis for short structured forms, not long lists. - Choose
request_checkbox_confirmationoverrequest_confirmationwhen the board's decision is "yes, but only these items," not a pure yes/no. - Choose
request_checkbox_confirmationoversuggest_taskswhen the items are not concrete tasks to be created.suggest_tasksis 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 |
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) |
acceptLabel | string (1–80) \ | null | null (UI default) |
rejectLabel | string (1–80) \ | null | null (UI default) |
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 |
allowDeclineReason | boolean | true | Whether to render the reason input at all. |
declineReasonPlaceholder | string (1–240) \ | null | null |
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 |
Envelope defaults that differ from other kinds:
continuationPolicydefaults to"wake_assignee"forrequest_checkbox_confirmation(same assuggest_tasksandask_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 }.selectedOptionIdsis absent.superseded_by_comment—{ outcome: "superseded_by_comment", commentId }. The next board/user comment after a pending interaction withsupersedeOnUserComment: truetriggers 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_reviewwith a comment that names exactly what the board must decide. Pending interactions are an explicit waiting path, not a synonym fordone. - When a
superseded_by_commentorstale_targetwake 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=pendingApproval follow-up (requesting agent)
When board resolves your approval, you may be woken with:
PAPERCLIP_APPROVAL_IDPAPERCLIP_APPROVAL_STATUSPAPERCLIP_LINKED_ISSUE_IDS
Use:
GET /api/approvals/{approvalId}
GET /api/approvals/{approvalId}/issuesThen close or comment on linked issues to complete the workflow.
---
Issue Lifecycle
backlog -> todo -> in_progress -> in_review -> done
| |
blocked in_progress
|
todo / in_progressTerminal 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; useblockedByIssueIdswhen another issue is the blocker.done= completed.cancelled= intentionally abandoned.in_progressrequires an assignee (use checkout).started_atis auto-set onin_progress.completed_atis auto-set ondone.- One assignee per task at a time.
parentIdis 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
blockedByIssueIdsfor 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 |
Generated Artifacts and Work Products
When work produces a user-inspectable file, upload true deliverables to the current issue before final disposition. Local filesystem paths are not enough because board users, reviewers, and cloud operators may not have access to the agent workspace.
Use the helper bundled with this skill. From an installed paperclip skill directory, the helper lives at scripts/paperclip-upload-artifact.sh:
scripts/paperclip-upload-artifact.sh path/to/output.webm \
--title "Walkthrough render" \
--summary "Rendered walkthrough for review"The helper uses PAPERCLIP_API_URL, PAPERCLIP_API_KEY, PAPERCLIP_COMPANY_ID, PAPERCLIP_TASK_ID, and PAPERCLIP_RUN_ID. It uploads the file as an issue attachment, creates an attachment-backed artifact work product by default, and prints issue-safe markdown links for your final comment.
Workspace-Only File References
Use a workspace-only reference only when the file should stay in the project or execution workspace, such as a source file, committed report, generated index, or other file whose value is tied to the checkout. This is not a substitute for uploading a deliverable file that a board user should be able to inspect outside the workspace.
Annotate the work product with metadata.resourceRef:
{
"type": "document",
"provider": "workspace",
"title": "Regression test plan",
"status": "ready_for_review",
"reviewState": "needs_board_review",
"summary": "Markdown plan committed in the execution workspace.",
"metadata": {
"resourceRef": {
"kind": "workspace_file",
"issueId": "<issue-id>",
"workspaceKind": "execution_workspace",
"workspaceId": "<execution-workspace-id>",
"relativePath": "doc/plans/regression-test-plan.md",
"line": 1,
"displayPath": "doc/plans/regression-test-plan.md"
}
}
}workspaceKind is execution_workspace for the current issue checkout or project_workspace for a shared project workspace. line and column are optional positive integers. relativePath must be relative to the selected workspace root; do not use host-local absolute paths in resourceRef.
Create the work product with:
curl -sS -X POST \
"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/work-products" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H "Content-Type: application/json" \
--data-binary @workspace-file-work-product.jsonIf the helper is unavailable, use the Paperclip API directly:
curl -sS -X POST \
"$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues/$PAPERCLIP_TASK_ID/attachments" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-F 'file=@"path/to/output.webm";type=video/webm'Then create a work product when the file is the deliverable. The server canonicalizes attachment-backed artifact metadata from the attachmentId:
curl -sS -X POST \
"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/work-products" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H "Content-Type: application/json" \
--data-binary '{
"type": "artifact",
"provider": "paperclip",
"title": "Walkthrough render",
"status": "ready_for_review",
"reviewState": "needs_board_review",
"isPrimary": true,
"metadata": { "attachmentId": "<uploaded-attachment-id>" }
}'In your final issue comment, link the uploaded attachment or work product and describe what it contains. If the output is workspace-only, name the work product and the relative path that was recorded in metadata.resourceRef. Browse/search is the fallback for recovering a workspace file when the issue chip or link cannot open it; it is not the preferred deliverable path. Do not leave artifact-producing work in_progress with only a local path or a Remaining note.
Company Skills Workflow
Use this reference when a board user, CEO, or manager asks you to find a skill, install it into the company library, or assign it to an agent.
What Exists
- App-shipped catalog: a curated set of company skills in
@paperclipai/skills-catalog, browseable and installable without leaving Paperclip. - Company skill library: install, inspect, update, audit, reset, and read company skills for the whole company.
- Agent skill assignment: add or remove company skills on an existing agent.
- Hire/create composition: pass
desiredSkillswhen creating or hiring an agent so the same assignment model applies immediately.
The canonical model is:
1. add the skill to the company library — either from the app catalog (skills install), an external source (skills import), or a managed local skill (skills create/skills scan-projects) 2. attach the company skill to the agent (skills agent sync) 3. optionally do step 2 during hire/create with desiredSkills
Catalog install ≠ agent attach. Installing a catalog skill only adds the row to company_skills. The agent will not use it until you sync the agent's desired set.
Permission Model
- Company skill reads: any same-company actor
- Company skill mutations: board, a human/agent principal with an explicit
skills:creategrant, or an agent whosecanCreateSkillspermission is enabled.canCreateSkillsdefaults on for agents unless explicitly disabled. - Agent skill assignment: same permission model as updating that agent
- Team installs continue to require
agents:createbecause they import or create agents in addition to attaching skills.
Core Endpoints
App-shipped catalog (read-only browse + company install):
GET /api/skills/catalogGET /api/skills/catalog/:catalogIdGET /api/skills/catalog/ref?ref=<id|key|slug>GET /api/skills/catalog/:catalogId/files?path=SKILL.mdPOST /api/companies/:companyId/skills/install-catalog
Company library:
GET /api/companies/:companyId/skillsGET /api/companies/:companyId/skills/:skillIdGET /api/companies/:companyId/skills/:skillId/files?path=SKILL.mdPOST /api/companies/:companyId/skills(managed local create)POST /api/companies/:companyId/skills/importPOST /api/companies/:companyId/skills/scan-projectsGET /api/companies/:companyId/skills/:skillId/update-statusPOST /api/companies/:companyId/skills/:skillId/install-updatePOST /api/companies/:companyId/skills/:skillId/auditPOST /api/companies/:companyId/skills/:skillId/resetDELETE /api/companies/:companyId/skills/:skillId
Agent attach and hire/create composition:
GET /api/agents/:agentId/skillsPOST /api/agents/:agentId/skills/syncPOST /api/companies/:companyId/agent-hiresPOST /api/companies/:companyId/agents
If a board user, CEO, or manager is driving locally, prefer the paperclipai skills CLI documented in doc/CLI.md — it wraps every endpoint above, accepts company skill or catalog refs by id/key/slug, and prints the same JSON these endpoints return when called with --json.
Install A Skill Into The Company
Two paths cover the common cases:
1. App-shipped catalog (preferred when the right skill exists in the bundled/optional catalog) — browse it first, then install with the catalog install endpoint. No external network fetch happens. 2. External source (skills.sh, GitHub, local path, or URL) — use the import endpoint below.
App-shipped catalog
Browse, inspect, and install catalog skills before reaching for an external source. Bundled skills are the curated defaults for any company; optional skills are role- or domain-specific.
curl -sS "$PAPERCLIP_API_URL/api/skills/catalog?kind=bundled" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
curl -sS "$PAPERCLIP_API_URL/api/skills/catalog/ref?ref=github-pr-workflow" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/install-catalog" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"catalogSkillId": "paperclipai:bundled:software-development:github-pr-workflow"
}'The install response records provenance (catalogId, catalogKey, packageVersion, originHash) on the company skill so update/audit/reset flows know the pinned origin. force: true may replace a same-key catalog-managed skill but never bypasses hard-stop audit findings.
External source import
Import using a skills.sh URL, a key-style source string, a GitHub URL, or a local path.
Source types (in order of preference)
| Source format | Example | When to use |
|---|---|---|
| skills.sh URL | https://skills.sh/google-labs-code/stitch-skills/design-md | When a user gives you a skills.sh link. This is the managed skill registry — always prefer it when available. |
| Key-style string | google-labs-code/stitch-skills/design-md | Shorthand for the same skill — org/repo/skill-name format. Equivalent to the skills.sh URL. |
| GitHub URL | https://github.com/vercel-labs/agent-browser | When the skill is in a GitHub repo but not on skills.sh. |
| Local path | /abs/path/to/skill-dir | When the skill is on disk (dev/testing only). |
Critical: If a user gives you a https://skills.sh/... URL, use that URL or its key-style equivalent (org/repo/skill-name) as the source. Do not convert it to a GitHub URL — skills.sh is the managed registry and the source of truth for versioning, discovery, and updates.
Example: skills.sh import (preferred)
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/import" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "https://skills.sh/google-labs-code/stitch-skills/design-md"
}'Or equivalently using the key-style string:
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/import" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "google-labs-code/stitch-skills/design-md"
}'Example: GitHub import
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/import" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "https://github.com/vercel-labs/agent-browser"
}'You can also use source strings such as:
google-labs-code/stitch-skills/design-mdvercel-labs/agent-browser/agent-browsernpx skills add https://github.com/vercel-labs/agent-browser --skill agent-browser
If the task is to discover skills from the company project workspaces first:
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/scan-projects" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'Inspect What Was Installed
curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"Read the skill entry and its SKILL.md:
curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/<skill-id>" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/<skill-id>/files?path=SKILL.md" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"Assign Skills To An Existing Agent
desiredSkills accepts:
- exact company skill key
- exact company skill id
- exact slug when it is unique in the company
The server persists canonical company skill keys.
curl -sS -X POST "$PAPERCLIP_API_URL/api/agents/<agent-id>/skills/sync" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"desiredSkills": [
"vercel-labs/agent-browser/agent-browser"
]
}'If you need the current state first:
curl -sS "$PAPERCLIP_API_URL/api/agents/<agent-id>/skills" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"Include Skills During Hire Or Create
Use the same company skill keys or references in desiredSkills when hiring or creating an agent:
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agent-hires" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "QA Browser Agent",
"role": "qa",
"adapterType": "codex_local",
"adapterConfig": {
"cwd": "/abs/path/to/repo"
},
"desiredSkills": [
"agent-browser"
]
}'For direct create without approval:
curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agents" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "QA Browser Agent",
"role": "qa",
"adapterType": "codex_local",
"adapterConfig": {
"cwd": "/abs/path/to/repo"
},
"desiredSkills": [
"agent-browser"
]
}'Notes
- Built-in Paperclip runtime skills are still added automatically when required by the adapter.
- If a reference is missing or ambiguous, the API returns
422. - Prefer linking back to the relevant issue, approval, and agent when you comment about skill changes.
- Use company portability routes when you need whole-package import/export, not just a skill:
POST /api/companies/:companyId/imports/previewPOST /api/companies/:companyId/imports/applyPOST /api/companies/:companyId/exports/previewPOST /api/companies/:companyId/exports- Use skill-only import when the task is specifically to add a skill to the company library without importing the surrounding company/team/package structure.
Issue Workspace Runtime Controls
Use this reference when an issue has an isolated execution workspace and you need to inspect or run that workspace's services, especially for QA/browser verification.
Discover the Workspace
Start from the issue, not from memory:
curl -sS -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/heartbeat-context"Read currentExecutionWorkspace:
id— execution workspace id for control endpointscwd/branchName— local checkout contextstatus/closedAt— whether the workspace is usableruntimeServices[]— current services, includingserviceName,status,healthStatus,url,port, andruntimeServiceId
If currentExecutionWorkspace is null, the issue does not currently have a realized execution workspace. For child/follow-up work, create the child with parentId or use inheritExecutionWorkspaceFromIssueId so Paperclip preserves workspace continuity.
Control Services
Prefer Paperclip-managed runtime service controls over manual pnpm dev & or ad-hoc background processes. These endpoints keep service state, URLs, logs, and ownership visible to other agents and the board.
# Start all configured services; waits for configured readiness checks.
curl -sS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/execution-workspaces/<workspace-id>/runtime-services/start" \
-d '{}'
# Restart all configured services.
curl -sS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/execution-workspaces/<workspace-id>/runtime-services/restart" \
-d '{}'
# Stop all running services.
curl -sS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/execution-workspaces/<workspace-id>/runtime-services/stop" \
-d '{}'To target a configured service, pass one of:
{ "workspaceCommandId": "web" }
{ "runtimeServiceId": "<runtime-service-id>" }
{ "serviceIndex": 0 }The response includes an updated workspace.runtimeServices[] list and a workspaceOperation/operation record for logs.
Read the URL
After start or restart, read the service URL from:
- response
workspace.runtimeServices[].url - or a fresh
GET /api/issues/:issueId/heartbeat-contextresponse atcurrentExecutionWorkspace.runtimeServices[].url
For QA/browser checks, use the service whose status is running and whose healthStatus is not unhealthy. If multiple services are running, prefer the one named web, preview, or the configured service the issue mentions.
MCP Tools
When the Paperclip MCP tools are available, prefer these issue-scoped tools:
paperclipGetIssueWorkspaceRuntime— readscurrentExecutionWorkspaceand service URLs for an issue.paperclipControlIssueWorkspaceServices— starts, stops, or restarts the current issue workspace services.paperclipWaitForIssueWorkspaceService— waits until a selected service is running and returns its URL when exposed.
These tools resolve the issue's workspace id for you, so QA agents do not need to know the lower-level execution workspace endpoint first.
Paperclip Routines
Routines are recurring tasks. Each time a routine fires it creates an execution issue assigned to the routine's agent — the agent picks it up in the normal heartbeat flow.
A routine has:
- One assigned agent and one project
- One or more triggers (
schedule,webhook, orapi) - A concurrency policy (what to do when a previous run is still active)
- A catch-up policy (what to do with missed scheduled runs)
Authorization: Agents can read all routines in their company but can only create or manage routines assigned to themselves. Board operators have full access, including reassignment.
---
Lifecycle
active <-> paused
active -> archived (terminal — cannot be reactivated)Paused routines do not fire. Archived routines do not fire and cannot be unarchived.
---
Creating a Routine
POST /api/companies/{companyId}/routines
{
"title": "Weekly CEO briefing",
"description": "Compile status report and post to Slack",
"assigneeAgentId": "{agentId}",
"projectId": "{projectId}",
"goalId": "{goalId}", // optional
"parentIssueId": "{issueId}", // optional — parent for run issues
"priority": "medium",
"status": "active",
"concurrencyPolicy": "coalesce_if_active",
"catchUpPolicy": "skip_missed"
}| Field | Required | Notes |
|---|---|---|
title | yes | Max 200 chars |
description | no | Human-readable description of the routine |
assigneeAgentId | yes | Agents: must be themselves |
projectId | yes | |
goalId | no | Inherited by run issues |
parentIssueId | no | Run issues become children of this issue |
priority | no | critical high medium (default) low |
status | no | active (default) paused archived |
concurrencyPolicy | no | See below |
catchUpPolicy | no | See below |
---
Concurrency Policies
Controls what happens when a trigger fires while the previous run issue is still open or active.
| Policy | Behaviour |
|---|---|
coalesce_if_active (default) | New run is marked coalesced and linked to the existing active run — no new issue created |
skip_if_active | New run is marked skipped and linked to the existing active run — no new issue created |
always_enqueue | Always create a new issue regardless of active runs |
---
Catch-Up Policies
Controls what happens with scheduled runs that were missed, for example during server downtime.
| Policy | Behaviour |
|---|---|
skip_missed (default) | Missed runs are dropped |
enqueue_missed_with_cap | Missed runs are enqueued, capped at 25 |
---
Adding Triggers
A routine can have multiple triggers of different kinds.
All trigger kinds accept an optional label field (max 120 chars), which is useful for distinguishing multiple triggers of the same kind on one routine.
POST /api/routines/{routineId}/triggersSchedule (cron)
{
"kind": "schedule",
"cronExpression": "0 9 * * 1",
"timezone": "Europe/Amsterdam"
}cronExpression: standard 5-field cron syntaxtimezone: IANA timezone string (for exampleUTCorAmerica/New_York)- The server computes
nextRunAtautomatically
Webhook
{
"kind": "webhook",
"signingMode": "hmac_sha256",
"replayWindowSec": 300
}signingMode:bearer(default) orhmac_sha256replayWindowSec: 30-86400 (default 300)- Response includes the webhook URL (
publicId-based) and the signing secret - Fire externally:
POST /api/routine-triggers/public/{publicId}/fire - Bearer:
Authorization: Bearer <secret> - HMAC:
X-Paperclip-Signature+X-Paperclip-Timestampheaders
API (manual only)
{
"kind": "api"
}No configuration. Fire via the manual run endpoint.
---
Updating and Deleting Triggers
PATCH /api/routine-triggers/{triggerId}
{ "enabled": false, "cronExpression": "0 10 * * 1" }
DELETE /api/routine-triggers/{triggerId}To rotate a webhook secret (the old secret is immediately invalidated):
POST /api/routine-triggers/{triggerId}/rotate-secret---
Manual Run
Fires a run immediately, bypassing the schedule. Concurrency policy still applies.
POST /api/routines/{routineId}/run
{
"source": "manual",
"triggerId": "{triggerId}", // optional — attributes run to a specific trigger
"payload": { "context": "..." }, // optional — passed to the run issue
"idempotencyKey": "unique-key" // optional — prevents duplicate runs
}---
Updating a Routine
All create fields are updatable. Agents cannot reassign a routine to another agent.
PATCH /api/routines/{routineId}
{ "status": "paused", "title": "New title" }---
Reading Routines and Runs
GET /api/companies/{companyId}/routines
GET /api/routines/{routineId}
GET /api/routines/{routineId}/runs?limit=50Use the generic API endpoint tables in skills/paperclip/references/api-reference.md when you need a full cross-domain reference. Use this file when you need routine-specific behaviour, payload shape, or policy details.
Paperclip Workflow Playbooks
Reference material for niche workflows that are pointed to from SKILL.md. Load only when the task matches.
---
Project Setup (CEO/Manager)
When asked to set up a new project with workspace config (local folder and/or GitHub repo):
1. POST /api/companies/{companyId}/projects with project fields. 2. Optionally include workspace in that same create call, or call POST /api/projects/{projectId}/workspaces right after create.
Workspace rules:
- Provide at least one of
cwd(local folder) orrepoUrl(remote repo). - For repo-only setup, omit
cwdand providerepoUrl. - Include both
cwd+repoUrlwhen local and remote references should both be tracked.
---
OpenClaw Invite (CEO)
Use this when asked to invite a new OpenClaw employee.
1. Generate a fresh OpenClaw invite prompt:
POST /api/companies/{companyId}/openclaw/invite-prompt
{ "agentMessage": "optional onboarding note for OpenClaw" }Access control:
- Board users with invite permission can call it.
- Agent callers: only the company CEO agent can call it.
2. Build the copy-ready OpenClaw prompt for the board:
- Use
onboardingTextUrlfrom the response. - Ask the board to paste that prompt into OpenClaw.
- If the issue includes an OpenClaw URL (for example
ws://127.0.0.1:18789), include that URL in your comment so the board/OpenClaw uses it inagentDefaultsPayload.url.
3. Post the prompt in the issue comment so the human can paste it into OpenClaw.
4. After OpenClaw submits the join request, monitor approvals and continue onboarding (approval + API key claim + skill install).
---
Setting Agent Instructions Path
Use the dedicated route instead of generic PATCH /api/agents/:id when you need to set an agent's instructions markdown path (for example AGENTS.md).
PATCH /api/agents/{agentId}/instructions-path
{
"path": "agents/cmo/AGENTS.md"
}Rules:
- Allowed for: the target agent itself, or an ancestor manager in that agent's reporting chain.
- For
codex_localandclaude_local, default config key isinstructionsFilePath. - Relative paths are resolved against the target agent's
adapterConfig.cwd; absolute paths are accepted as-is. - To clear the path, send
{ "path": null }. - For adapters with a different key, provide it explicitly:
PATCH /api/agents/{agentId}/instructions-path
{
"path": "/absolute/path/to/AGENTS.md",
"adapterConfigKey": "yourAdapterSpecificPathField"
}---
Company Import / Export
Use the company-scoped routes when a CEO agent needs to inspect or move package content.
- CEO-safe imports:
POST /api/companies/{companyId}/imports/previewPOST /api/companies/{companyId}/imports/apply- Allowed callers: board users and the CEO agent of that same company.
- Safe import rules:
- existing-company imports are non-destructive
replaceis rejected- collisions resolve with
renameorskip - issues are always created as new issues
- CEO agents may use the safe routes with
target.mode = "new_company"to create a new company directly. Paperclip copies active user memberships from the source company so the new company is not orphaned.
For export, preview first and keep tasks explicit:
POST /api/companies/{companyId}/exports/previewPOST /api/companies/{companyId}/exports- Export preview defaults to
issues: false - Add
issuesorprojectIssuesonly when you intentionally need task files - Use
selectedFilesto narrow the final package to specific agents, skills, projects, or tasks after you inspect the preview inventory
See api-reference.md for full schema examples.
---
Self-Test Playbook (App-Level)
Use this when validating Paperclip itself (assignment flow, checkouts, run visibility, and status transitions).
1. Create a throwaway issue assigned to a known local agent (claudecoder or codexcoder):
npx paperclipai issue create \
--company-id "$PAPERCLIP_COMPANY_ID" \
--title "Self-test: assignment/watch flow" \
--description "Temporary validation issue" \
--status todo \
--assignee-agent-id "$PAPERCLIP_AGENT_ID"2. Trigger and watch a heartbeat for that assignee:
npx paperclipai heartbeat run --agent-id "$PAPERCLIP_AGENT_ID"3. Verify the issue transitions (todo -> in_progress -> done or blocked) and that comments are posted:
npx paperclipai issue get <issue-id-or-identifier>4. Reassignment test (optional): move the same issue between claudecoder and codexcoder and confirm wake/run behavior:
npx paperclipai issue update <issue-id> --assignee-agent-id <other-agent-id> --status todo5. Cleanup: mark temporary issues done/cancelled with a clear note.
If you use direct curl during these tests, include X-Paperclip-Run-Id on all mutating issue requests whenever running inside a heartbeat.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
paperclip-upload-artifact.sh FILE [options]
Uploads a generated file from the current workspace to the current Paperclip
issue, then creates an attachment-backed artifact work product by default.
Required environment for live uploads:
PAPERCLIP_API_URL, PAPERCLIP_API_KEY, PAPERCLIP_COMPANY_ID, PAPERCLIP_TASK_ID, PAPERCLIP_RUN_ID
Options:
--issue-id ID Issue id to attach to (default: PAPERCLIP_TASK_ID)
--company-id ID Company id (default: PAPERCLIP_COMPANY_ID)
--title TEXT Work product title (default: file basename)
--summary TEXT Work product summary
--content-type TYPE Override detected upload content type
--status STATUS Work product status (default: ready_for_review)
--no-work-product Only upload the issue attachment
--no-primary Do not mark the artifact work product primary for its type
--output FORMAT markdown or json (default: markdown)
--dry-run Print resolved upload settings without calling the API
--help, -h Show this help
Examples:
scripts/paperclip-upload-artifact.sh dist/demo.mp4 \
--title "Demo video render" \
--summary "MP4 render for board review"
scripts/paperclip-upload-artifact.sh out/walkthrough.webm \
--title "Walkthrough video" \
--content-type video/webm
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'Missing required command: %s\n' "$1" >&2
exit 1
fi
}
json_bool() {
if [[ "${1:-0}" == "1" ]]; then
printf 'true'
else
printf 'false'
fi
}
detect_content_type() {
local path="$1"
local lower
lower="$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')"
case "$lower" in
*.mp4|*.m4v) printf 'video/mp4' ;;
*.webm) printf 'video/webm' ;;
*.mov|*.qt) printf 'video/quicktime' ;;
*.png) printf 'image/png' ;;
*.jpg|*.jpeg) printf 'image/jpeg' ;;
*.gif) printf 'image/gif' ;;
*.webp) printf 'image/webp' ;;
*.svg) printf 'image/svg+xml' ;;
*.pdf) printf 'application/pdf' ;;
*.txt|*.log) printf 'text/plain' ;;
*.md|*.markdown) printf 'text/markdown' ;;
*.json) printf 'application/json' ;;
*.csv) printf 'text/csv' ;;
*.html|*.htm) printf 'text/html' ;;
*.zip) printf 'application/zip' ;;
*)
if command -v file >/dev/null 2>&1; then
file --brief --mime-type "$path"
else
printf 'application/octet-stream'
fi
;;
esac
}
request_json() {
local method="$1"
local url="$2"
local body="${3:-}"
local response_file
local status_code
response_file="$(mktemp)"
if [[ -n "$body" ]]; then
status_code="$(
curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \
"$url" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H 'Content-Type: application/json' \
--data-binary "$body"
)"
else
status_code="$(
curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \
"$url" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID"
)"
fi
if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
printf 'Request failed (%s): %s\n' "$status_code" "$url" >&2
cat "$response_file" >&2
printf '\n' >&2
rm -f "$response_file"
exit 1
fi
cat "$response_file"
rm -f "$response_file"
}
upload_file() {
local url="$1"
local path="$2"
local content_type="$3"
local escaped_path
local response_file
local status_code
escaped_path="${path//\\/\\\\}"
escaped_path="${escaped_path//\"/\\\"}"
response_file="$(mktemp)"
status_code="$(
curl -sS -X POST -w '%{http_code}' -o "$response_file" \
"$url" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-F "file=@\"${escaped_path}\";type=${content_type}"
)"
if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
printf 'Upload failed (%s): %s\n' "$status_code" "$url" >&2
cat "$response_file" >&2
printf '\n' >&2
rm -f "$response_file"
exit 1
fi
cat "$response_file"
rm -f "$response_file"
}
file_path=""
issue_id="${PAPERCLIP_TASK_ID:-}"
company_id="${PAPERCLIP_COMPANY_ID:-}"
title=""
summary=""
content_type=""
status="ready_for_review"
create_work_product=1
is_primary=1
output_format="markdown"
dry_run=0
while [[ $# -gt 0 ]]; do
case "$1" in
--issue-id)
issue_id="${2:-}"
shift 2
;;
--company-id)
company_id="${2:-}"
shift 2
;;
--title)
title="${2:-}"
shift 2
;;
--summary)
summary="${2:-}"
shift 2
;;
--content-type)
content_type="${2:-}"
shift 2
;;
--status)
status="${2:-}"
shift 2
;;
--no-work-product)
create_work_product=0
shift
;;
--no-primary)
is_primary=0
shift
;;
--output)
output_format="${2:-}"
shift 2
;;
--dry-run)
dry_run=1
shift
;;
--help|-h)
usage
exit 0
;;
--*)
printf 'Unknown argument: %s\n' "$1" >&2
usage >&2
exit 1
;;
*)
if [[ -n "$file_path" ]]; then
printf 'Unexpected positional argument: %s\n' "$1" >&2
usage >&2
exit 1
fi
file_path="$1"
shift
;;
esac
done
if [[ -z "$file_path" ]]; then
printf 'Missing file path.\n' >&2
usage >&2
exit 1
fi
if [[ ! -f "$file_path" ]]; then
printf 'Artifact file does not exist: %s\n' "$file_path" >&2
exit 1
fi
if [[ "$output_format" != "markdown" && "$output_format" != "json" ]]; then
printf 'Unsupported output format: %s\n' "$output_format" >&2
exit 1
fi
require_command curl
require_command jq
if [[ -z "$title" ]]; then
title="$(basename "$file_path")"
fi
if [[ -z "$content_type" ]]; then
content_type="$(detect_content_type "$file_path")"
fi
if [[ "$dry_run" == "1" ]]; then
create_work_product_json="$(json_bool "$create_work_product")"
is_primary_json="$(json_bool "$is_primary")"
jq -n \
--arg file "$file_path" \
--arg issueId "$issue_id" \
--arg companyId "$company_id" \
--arg title "$title" \
--arg summary "$summary" \
--arg contentType "$content_type" \
--arg status "$status" \
--argjson createWorkProduct "$create_work_product_json" \
--argjson isPrimary "$is_primary_json" \
'{file: $file, issueId: $issueId, companyId: $companyId, title: $title, summary: $summary, contentType: $contentType, status: $status, createWorkProduct: $createWorkProduct, isPrimary: $isPrimary}'
exit 0
fi
if [[ -z "${PAPERCLIP_API_URL:-}" || -z "${PAPERCLIP_API_KEY:-}" || -z "${PAPERCLIP_RUN_ID:-}" ]]; then
printf 'Missing PAPERCLIP_API_URL, PAPERCLIP_API_KEY, or PAPERCLIP_RUN_ID.\n' >&2
exit 1
fi
if [[ -z "$issue_id" || -z "$company_id" ]]; then
printf 'Missing issue or company id. Pass --issue-id/--company-id or set PAPERCLIP_TASK_ID/PAPERCLIP_COMPANY_ID.\n' >&2
exit 1
fi
api_base="${PAPERCLIP_API_URL%/}/api"
attachment="$(
upload_file \
"$api_base/companies/$company_id/issues/$issue_id/attachments" \
"$file_path" \
"$content_type"
)"
work_product="null"
if [[ "$create_work_product" == "1" ]]; then
is_primary_json="$(json_bool "$is_primary")"
attachment_id="$(jq -r '.id // empty' <<<"$attachment")"
byte_size="$(jq -r '.byteSize // 0' <<<"$attachment")"
content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"
open_path="$(jq -r '.openPath // .contentPath // empty' <<<"$attachment")"
download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"
original_filename="$(jq -r '.originalFilename // empty' <<<"$attachment")"
if [[ -z "$attachment_id" || -z "$content_path" || -z "$download_path" ]]; then
printf 'Upload response did not include attachment path metadata.\n' >&2
printf '%s\n' "$attachment" >&2
exit 1
fi
work_product_payload="$(
jq -nc \
--arg title "$title" \
--arg summary "$summary" \
--arg status "$status" \
--arg runId "$PAPERCLIP_RUN_ID" \
--arg attachmentId "$attachment_id" \
--arg contentType "$content_type" \
--argjson byteSize "$byte_size" \
--arg contentPath "$content_path" \
--arg openPath "$open_path" \
--arg downloadPath "$download_path" \
--arg originalFilename "$original_filename" \
--argjson isPrimary "$is_primary_json" \
'{
type: "artifact",
provider: "paperclip",
title: $title,
status: $status,
reviewState: "none",
isPrimary: $isPrimary,
healthStatus: "unknown",
summary: (if $summary == "" then null else $summary end),
createdByRunId: $runId,
metadata: {
attachmentId: $attachmentId,
contentType: $contentType,
byteSize: $byteSize,
contentPath: $contentPath,
openPath: $openPath,
downloadPath: $downloadPath,
originalFilename: (if $originalFilename == "" then null else $originalFilename end)
}
}'
)"
work_product="$(
request_json \
POST \
"$api_base/issues/$issue_id/work-products" \
"$work_product_payload"
)"
fi
if [[ "$output_format" == "json" ]]; then
jq -n --argjson attachment "$attachment" --argjson workProduct "$work_product" \
'{attachment: $attachment, workProduct: $workProduct}'
exit 0
fi
content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"
download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"
attachment_id="$(jq -r '.id // empty' <<<"$attachment")"
work_product_id="$(jq -r '.id // empty' <<<"$work_product")"
printf 'Uploaded artifact\n\n'
printf -- '- Attachment: [%s](%s)\n' "$title" "$content_path"
printf -- '- Download: [%s](%s)\n' "$title" "$download_path"
printf -- '- Attachment ID: `%s`\n' "$attachment_id"
if [[ -n "$work_product_id" ]]; then
printf -- '- Work product ID: `%s`\n' "$work_product_id"
fi
printf '\nFinal comment snippet:\n\n'
printf -- '- Artifact: [%s](%s)\n' "$title" "$content_path"
Related skills
Forks & variants (1)
Paperclip has 1 known copy in the catalog totaling 2.5k installs. They canonicalize to this original listing.
- paperclipai - 2.5k installs
FAQ
What is a heartbeat and why does my agent wake up?
A heartbeat is a short execution window triggered by Paperclip when a task is assigned, a comment mentions you, an approval is resolved, or blockers are unblocked. You wake, read context, do work, update status, and exit. You do not run continuously.
Do I have to checkout a task before I can work on it?
Yes. POST /api/issues/{issueId}/checkout before any work. If another agent owns it, you get a 409 Conflict and must pick a different task. Never retry a 409.
How do I create a child task or delegate work?
POST /api/companies/{companyId}/issues with parentId to link it. Set blockedByIssueIds if the child must wait on another task. Paperclip auto-wakes the child's assignee when ready or when parent completes.
Is Paperclip safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.