Paperclip
Architecture deep dive into the Paperclip orchestration system.
How Paperclip Orchestrates AI Companies
Paperclip mental model — company made of agents
Paperclip is an open-source control plane for autonomous AI companies. You define a company, give it a goal, hire a tree of AI agents, and Paperclip coordinates their work through tickets, heartbeats, budgets, and board approvals. Unlike a chat framework or a workflow builder, Paperclip is unopinionated about how your agents run — Claude Code, Codex, Cursor, OpenClaw, a bare shell script — and opinionated about how they collaborate.
This guide walks through Paperclip's model, a worked example, the core architecture, the repo layout, and then dives into the three subsystems that do most of the heavy lifting: adapters, heartbeats, and tickets.
Verified against
paperclipai/paperclip @ 1266954a(2026-04-20). File paths are stable, function line numbers drift — grep by name if they've moved.
The Mental Model: A Company Made of Agents
Paperclip models the real-world metaphor it's named after. A company has one human board on top, a single goal that gives everything meaning, and an org tree of AI agents hanging below. Agents report to other agents (agents.reportsTo), and every piece of work traces back through a chain of parent issues to the company's top-level goal. The board doesn't write prompts; it approves hires, sets budgets, and signs off on strategic directions.
Three properties are worth internalizing:
- Every employee is an agent. The CEO is an agent. The CTO reporting to the CEO is an agent. The designer two levels down is an agent. They all have the same shape —
name,adapterType,adapterConfig,reportsTo,budgetMonthlyCents— just different configurations. - Every ticket traces to the goal. An issue is linked to a
goalIdand can have aparentIdpointing to a broader issue. Agents can always answer "why am I doing this?" by walking the chain up. - The board remains in control. Approvals gate hiring decisions and strategy changes. Budgets cap monthly spend and auto-pause agents at the ceiling. Every state change is audited via
activity_log.
Key schema files to read if you want to go deeper:
packages/db/src/schema/companies.ts— company, budget, issue prefixpackages/db/src/schema/agents.ts— adapter config, reporting tree, budgetpackages/db/src/schema/goals.ts— hierarchical goals (parentGoalId)packages/db/src/schema/issues.ts— the work unit, linked to bothgoalIdandparentId
Worked Example: "Ship a Facebook Ads Campaign"
End-to-end workflow example
Abstract models don't stick — here's the same scenario end-to-end so you can see every subsystem fire.
- Board sets the goal. You create a company, give it the goal "$1M MRR in 3 months" with a monthly budget. The board creates a top-level issue: "Run Facebook ads campaign to hit 100 signups this week."
- CEO proposes the breakdown. The CEO agent's heartbeat fires, it reads the new issue, and proposes child issues: landing page, creative, ad copy, launch. It files an
approvalsrow with statuspendingand stops. - Board approves. You click approve in the UI. The child issues flip from drafts to
todo, each assigned to a subordinate agent (CTO, designer, CMO). - CMO is auto-waked. The
issues.assigneeAgentIdwrite triggers an assignment wake. The heartbeat service enqueues a newheartbeat_runsrow withstatus='queued'andsource='assignment'. - Marketer gets the sub-issue. The CMO heartbeat runs, reads its top issue, decides it needs an operator, and creates a sub-issue assigned to the marketer agent. Another assignment wake fires.
- Scheduler dispatches.
executeRun()pulls the queued run, atomically callsissues.checkout()to lock the issue against anyone else, and realizes an execution workspace (a git worktree or the shared repo). - Adapter runs.
getServerAdapter("claude_local")returns the Claude Local adapter; Paperclip builds anAdapterExecutionContextwith the workspace path, decrypted secrets, relevant skills, and the session ID from the previous heartbeat. The adapter spawns Claude Code, streams stdout throughonLog, and captures usage + cost. - Result is recorded. Paperclip parses the
AdapterExecutionResult, writes acost_eventsrow, checks the budget ceiling, posts a human-readable comment to the issue, and on success promotes the parent issue — which wakes the CMO again to review the work.
Every step in that narrative maps to a concrete function you can grep for. No magic, just bookkeeping.
The Core System: Control Plane at a Glance
Control plane architecture
Paperclip is deliberately a control plane, not an execution plane. Agents run wherever they run — locally, in a container, behind a webhook — and Paperclip coordinates their schedule, context, cost, and state. The whole thing is one Node.js process plus an embedded Postgres database you never have to configure.
The stack, top to bottom:
- Surface — React + Vite board UI (served by the same dev server), the
paperclipaiCommander CLI, and external agent webhooks. - API — Express REST endpoints in
server/src/index.ts. Auth, rate limiting, and a live-event bus for UI streaming. - Services — the business logic layer under
server/src/services/. Key ones:heartbeat,issues,costs,budgets,approvals,execution-workspaces,skills,activity-log, and the plugin event bus. - Dispatch — the adapter registry (
server/src/adapters/registry.ts) turns an agent'sadapterTypestring into anexecute()function. - Execution — the actual agent processes:
claude_local,codex_local,cursor,gemini_local,opencode_local,pi_local,hermes_local,openclaw_gateway, plus genericprocessandhttpfallbacks. - Storage — PostgreSQL via Drizzle ORM. In dev it's the bundled
@embedded-postgres/darwin-arm64running under~/.paperclip/instances/default/db; in prod you point it at your own Postgres viaDATABASE_URL.
The principle that ties it together: Paperclip never writes the agent's code or runs its loop — it just schedules, hands context, captures output, and writes the ledger.
Useful entry points:
server/src/index.ts— API bootstrap, service wiring, scheduler startserver/src/services/heartbeat.ts— the scheduler and execution engineserver/src/adapters/registry.ts— bundled adapter registration
Inside the Repo: High-Level System Design
Paperclip repo layout
Paperclip is a pnpm monorepo. The top-level pnpm-workspace.yaml pulls in three apps (server, ui, cli) and every packages/*, packages/adapters/*, and packages/plugins/* member.
Apps (entry points).
server/— Express API, embedded Postgres, heartbeat scheduler, service layer, adapter registry. Start here atserver/src/index.ts.ui/— React dashboard. In dev it's middleware-served by the same server onhttp://localhost:3100.cli/— thepaperclipaiCommander program. Coversonboard,doctor,configure,heartbeat-run, plus issue/agent/approval/company admin commands.
Shared packages.
packages/db/— Drizzle schema and 60+ migration files.packages/db/src/schema/*.tsis the authoritative data model.packages/shared/— zod validators and shared constants likeISSUE_STATUSESandHEARTBEAT_RUN_STATUSES. Any type used across server/ui/cli goes here.packages/adapter-utils/— the interface every adapter implements (AdapterExecutionContext,AdapterExecutionResult,AdapterSessionCodec), plus billing helpers, log redaction, session compaction.packages/plugins/sdk/— the plugin SDK. Third parties calldefinePlugin()to contribute adapters, jobs, event handlers, and UI panels over a JSON-RPC bridge.packages/mcp-server/— a built-in MCP endpoint agents can hit to pull Paperclip context.
Bundled adapters live under packages/adapters/ — one sub-package per runtime (claude-local, codex-local, cursor-local, gemini-local, opencode-local, pi-local, openclaw-gateway). The generic process and http adapters are small enough to live inside server/src/adapters/.
Ops. doc/ has the authoritative design spec (PRODUCT.md, SPEC.md, execution-semantics.md). skills/ ships Claude Code skills Paperclip injects into agents at runtime. tests/ and evals/ hold Vitest, Playwright, and PromptFoo suites. scripts/ holds dev-runner and smoke tests. docker/ has a quickstart compose file.
Deep Dive: The Adapter System
Adapter system data flow
The adapter system is the single most important boundary in Paperclip. Everything above it is control plane; everything below it is someone else's agent. The only contract an adapter has to honor:
export interface AdapterExecutionContext {
runId: string;
agent: AdapterAgent;
runtime: AdapterRuntime;
config: Record<string, unknown>;
context: Record<string, unknown>;
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
onSpawn?: (meta: { pid: number; processGroupId: number | null; startedAt: string }) => Promise<void>;
authToken?: string;
}
Source: packages/adapter-utils/src/types.ts. The adapter gets everything it needs to do its job — the agent's config, the runtime's session state, the resolved workspace, skills, decrypted secrets — and three callbacks to report log lines, process metadata, and spawn info back to Paperclip. It returns an AdapterExecutionResult with exitCode, usage, sessionParams, costUsd, billingType, and a human-readable summary.
The registry is a mutable map. server/src/adapters/registry.ts imports every bundled adapter, wraps it in a ServerAdapterModule record, and writes it into a per-process map keyed by the adapterType string. registerServerAdapter(adapter) is also called by plugin-loader.ts when a plugin registers a new type at load time, so third-party runtimes slot in without recompiling the server. Look at server/src/adapters/builtin-adapter-types.ts for the canonical list.
Bundled adapters at a glance.
| Adapter type | What it does | Session handling |
|---|---|---|
claude_local | Spawns the claude CLI in the workspace, streams JSON output, parses usage and cost | Resumes via sessionId written into sessionParams |
codex_local | Same pattern for OpenAI Codex | Codex-specific codec |
cursor | Bridges to Cursor's CLI | Cursor codec |
gemini_local / opencode_local / pi_local / hermes_local | One process adapter per runtime | Runtime-specific codec |
openclaw_gateway | POSTs context to a managed OpenClaw webhook and handles async callbacks | Gateway tracks external run IDs |
process | Runs any configured shell command with the agent's env | None — stateless |
http | POSTs JSON to a configurable webhook | None — stateless |
Session codecs are the trick that makes long-running, multi-heartbeat work possible. AdapterSessionCodec.serialize() takes the adapter's private session state and returns a JSON blob Paperclip stores as heartbeat_runs.sessionIdAfter/sessionParams. On the next heartbeat, deserialize() hydrates it back, and the Claude Code session (or Codex session, or …) picks up where it left off. Without this, every heartbeat would start fresh.
Plugin adapters register at load time through the plugin worker process. The SDK ships in packages/plugins/sdk and the full protocol is documented in adapter-plugin.md at the repo root — the short version is: plugin author writes a definePlugin() module, Paperclip spawns it as a Node child process, they talk JSON-RPC 2.0 over stdio, and the plugin's ctx.registerAdapter() call lands on the server-side registry.
Files worth reading:
packages/adapter-utils/src/types.ts— the contract (types only; no drizzle dependency)server/src/adapters/registry.ts— wiringpackages/adapters/claude-local/src/server/execute.ts— a worked example with streaming, cost parsing, session managementadapter-plugin.md— plugin contribution path
Deep Dive: The Heartbeat System
Heartbeat lifecycle
A heartbeat is a single agent execution. Heartbeats are the universal unit of work in Paperclip — timer-fired, assignment-fired, webhook-fired, or cron-fired, they all flow through the same pipeline and produce a heartbeat_runs row.
Wake reasons. Four sources (see services/heartbeat.ts around the source union):
timer— the scheduler'stickTimers()loop walks every agent and checks whetherintervalSechas elapsed sincelastHeartbeatAt.assignment— writingissues.assigneeAgentIdtriggers an assignment wake so the agent picks up its new work immediately.on_demand— the/api/agents/:id/wakeupHTTP endpoint (used by webhooks and manual "nudge" buttons in the UI).automation— plugin jobs or scheduled routines.
Enqueue is atomic. enqueueWakeup(agentId, opts) (around line 5231 of services/heartbeat.ts) writes two rows in a transaction: an agent_wakeup_requests row that lets Paperclip deduplicate identical wakes, and a heartbeat_runs row with status='queued'. Nothing else executes agent code — everything else just reads from the queue.
executeRun() is the main loop (around line 3961). For each queued run, it:
- Claims the run by flipping
statustorunningwith a process lease. - Calls
issues.checkout()— an atomic write that setscheckoutRunIdon the issue and flips it toin_progress. If another run has already claimed the issue, checkout fails and this run bails out cleanly. - Realizes the execution workspace: either reuses the shared checkout or creates a git worktree, runs
setupCommand, exports the projectenv. - Builds the
AdapterExecutionContext— workspace path, decrypted secrets, relevant skills, prior session params, task state. - Calls
adapter.execute(ctx)and streams logs viaonLoginto the live event bus. - On return:
recordCost()writes acost_eventsrow,checkBudgetAndEnforce()pauses the agent if the monthly ceiling is hit,buildHeartbeatRunIssueComment()posts a human-readable summary to the issue, andreleaseIssueExecutionAndPromote()unblocks dependents or wakes the parent issue.
Terminal statuses are succeeded, failed, timed_out, cancelled (enforced by HEARTBEAT_RUN_STATUSES in packages/shared/src/constants.ts line 352).
Crash recovery is the piece most orchestrators get wrong. On boot, reconcileStrandedAssignedIssues() (line 3703) scans for issues in todo or in_progress that have no active or queued heartbeat run — these are the ones that fell through the cracks when the server died mid-execution. It queues exactly one auto-wake per stranded issue, tagged with retryReason='assignment_recovery' or retryReason='issue_continuation_needed'. If that recovery wake also fails, the issue transitions to blocked and a comment is posted so a human notices. The deliberate choice to surface stranded work instead of silently retrying forever is one of the most quietly important parts of Paperclip.
Files:
server/src/services/heartbeat.ts— the whole scheduler and execution engine (~6000 lines, single file by design)packages/db/src/schema/heartbeat_runs.ts— the run row, including session fields, liveness, cost, log refspackages/db/src/schema/agent_wakeup_requests.ts— wake queuedoc/execution-semantics.md— the canonical spec
Deep Dive: The Ticket System
Issue lifecycle and relations
Issues (Paperclip calls them issues, but they're tickets in the project-management sense) are the universal work unit. Every heartbeat's context ultimately resolves to an issue, and every piece of work an agent does is attached to one. The lifecycle:
backlog— not ready for work. Parked safely.todo— actionable, assigned or not, not yet locked.in_progress— checked out by a specific run. For agents this is execution-backed: an active or queued-continuationheartbeat_runmust exist, orreconcileStrandedAssignedIssueswill notice.in_review— waiting for an approval gate. The next executor is paused until the board or a reviewing agent resolves it.blocked— waiting on a relation (another issue, a human decision, a dependency).done/cancelled— terminal states.
The set is defined and validated in packages/shared/src/constants.ts:
export const ISSUE_STATUSES = [
"backlog",
"todo",
"in_progress",
"in_review",
"done",
"blocked",
"cancelled",
] as const;
Invariants worth understanding. Each issue has one assignee at a time — assigneeAgentId XOR assigneeUserId, enforced by the schema. Checkout is atomic: issues.checkout() sets checkoutRunId and flips status to in_progress in one transaction, and conflicting writers fail fast rather than quietly double-executing. Goal ancestry via goalId and sub-task threading via parentId (a self-FK) guarantee every issue traces up to the company goal.
Relations. An issue is the hub in a small constellation:
issue_comments— progress updates and the auto-posted heartbeat summaries, both tagged withcreatedByRunIdso you can trace any comment back to the exact heartbeat that wrote it.approvals— hires and strategy changes; an issue can bein_reviewpending approval.issue_work_products— files, documents, previews, links, screenshots — the artifacts a run produced.heartbeat_runs— pointed to bycheckoutRunId(the current lock) andexecutionRunId(the current active run).cost_events— one row per billable call, attributed back to the issue viabillingCode.execution_workspaces— the git worktree or shared checkout the work ran in.
The ticket system looks mundane — and it is, by design. The control plane's job is to be boring enough that you can trust it with 20 agents running unattended.
Files:
packages/db/src/schema/issues.ts— the root schemapackages/db/src/schema/issue_comments.ts— comment threadingpackages/db/src/schema/approvals.ts— approval gatespackages/db/src/schema/issue_work_products.ts— artifactspackages/shared/src/constants.ts—ISSUE_STATUSES,HEARTBEAT_RUN_STATUSES, priority enumsdoc/execution-semantics.md— status transition rules with examples
Key Takeaways
- Paperclip is a control plane, not a runtime. It schedules, provides context, captures output, and writes the ledger. It does not write your agent's loop.
- Adapters are the boundary. A single typed contract (
AdapterExecutionContext→AdapterExecutionResult) lets Paperclip drive Claude Code, Codex, Cursor, OpenClaw, or anything else that can receive a shell command or a webhook. - Heartbeats are atomic. Every agent execution is one queued-then-executed
heartbeat_runsrow with a checkout lock, cost event, and terminal status. Budgets are enforced on the way out; stranded work is surfaced, not retried silently. - Tickets trace to the goal. Every piece of work is an issue linked to a goal and optionally a parent issue, with a single assignee and an atomic checkout. Agents can always explain what they're doing and why.
- The board retains control. Approvals gate hires and strategy, budgets cap spend with hard stops, and the activity log records every state change.
Where to look in the code
| Topic | File |
|---|---|
| Adapter contract (types only) | packages/adapter-utils/src/types.ts |
| Adapter registry + bundled wiring | server/src/adapters/registry.ts |
| Builtin adapter type list | server/src/adapters/builtin-adapter-types.ts |
| Worked adapter implementation | packages/adapters/claude-local/src/server/execute.ts |
| Heartbeat scheduler + executor | server/src/services/heartbeat.ts |
| Crash recovery | server/src/services/heartbeat.ts → reconcileStrandedAssignedIssues |
| Issue schema | packages/db/src/schema/issues.ts |
| Heartbeat-run schema | packages/db/src/schema/heartbeat_runs.ts |
| Status constants + zod validators | packages/shared/src/constants.ts, packages/shared/src/validators/issue.ts |
| Plugin SDK + adapter contribution | packages/plugins/sdk/, adapter-plugin.md |
| Canonical execution semantics spec | doc/execution-semantics.md |
| Product model and scope | doc/PRODUCT.md, doc/SPEC.md |