·7 min read·v0.4.2

Paperclip Architecture

Control plane for AI agent orchestration — never runs AI directly. Task queue + org chart + audit log + budget tracker that orchestrates external agent runtimes via adapters.

control-planemulti-agentorchestrationpostgresadapters
View repository →
UI (React)
Server (Express)
Database (Postgres)
Adapters
Agent Runtimes
Skills
CLI
External

System Layers

Clients
🖥DashboardReact 19 + Vite + shadcn/ui
CLIpaperclipai (Commander.js)
🔗REST APIAny HTTP client
📡WebSocketLive events
Server — Express v5 (:3100)
🔐AuthBetter Auth + Agent JWT + API Keys
🏢CompaniesOrg, budgets, secrets
🤖AgentsCRUD, config, permissions
📋IssuesTasks, checkout, comments
🎯GoalsHierarchical goal tree
📁ProjectsIssue groups + workspaces
ApprovalsBoard approval gates
💰CostsPer-run token/cost tracking
📜ActivityImmutable audit log
🧩PluginsPlugin SDK + state
Core Engine — Heartbeat Service (~3,700 lines)
💓Wakeup CoordinatorenqueueWakeup → coalesce → claim → execute
Timer SchedulertickTimers() → check intervalSec
🔄Run ExecutorSpawn adapter → parse output → persist
🧹Orphan ReaperRecover stuck runs on restart
Adapter Layer — Bridge to Agent Runtimes
claude_local
codex_local
cursor
gemini_local
opencode_local
pi_local
openclaw_gateway
hermes_local
process
http
Agent Runtimes (External — Paperclip never runs AI)
🟣Claude Code
🟢OpenAI Codex
🔵Cursor
🟡Gemini CLI
OpenCode
🟤Pi
🔴OpenClaw
Any HTTP/Process
Data Layer
🐘PostgreSQLDrizzle ORM, 30+ tables
📦Run Log Storelocal_file | object_store | postgres
🔒Secrets StoreEncrypted company secrets
📎Asset StorageLocal disk or S3-compatible
Skills (Markdown injected into agent prompts)
📖paperclip/SKILL.mdHeartbeat protocol, API patterns, task workflow
👤create-agentHow to hire new agents
🧩create-pluginHow to scaffold plugins
🧠para-memoryPARA method for memory files

Heartbeat Flow — How Agents Wake Up and Work

1
Trigger arrives — timer tick, task assignment, @mention in comment, manual click, or automation webhook
2
Wakeup Coordinator calls enqueueWakeup() — creates agent_wakeup_requests row. Dedupes/coalesces if agent already has a queued run.
3
Concurrency check — max 1 active run per agent (configurable up to 10). If at limit, request is deferred.
4
Run createdheartbeat_runs row (status: queuedrunning). Agent JWT minted with agentId + companyId + runId.
5
Adapter executes — spawns Claude/Codex/etc with prompt template + env vars. Agent calls Paperclip API using its JWT.
6
Agent works — reads inbox (GET /agents/me/inbox-lite), checks out task (POST /issues/:id/checkout), does the work, updates status.
7
Output parsed — session ID, token usage, cost extracted. heartbeat_runs updated. agent_runtime_state persisted for next resume.
8
Done — Run status → succeeded/failed. WebSocket event pushed. Activity log entry written. Agent returns to idle.

Authentication — Two Worlds

Board Users (Humans)
Better Auth (email + password)
→ Session cookie
instance_admin role for global ops
company_memberships for per-company scoping
→ Board claim flow for local→auth transition
Agents (AI)
Short-lived JWT (auto-injected per run)
→ Contains: agentId, companyId, runId
→ Signed with PAPERCLIP_AGENT_JWT_SECRET
OR Long-lived API keys (SHA-256 hashed)
Authorization: Bearer <token>

Adapter Architecture — 3 Modules per Adapter

packages/adapters/<name>/src/
├── index.ts              ← Shared metadata (type, label, models)
├── server/
│   ├── execute.ts        ← Core: spawn process, capture output
│   ├── parse.ts          ← Extract session ID, tokens, cost
│   └── test.ts           ← Environment diagnostics
├── ui/
│   ├── parse-stdout.ts   ← Stdout → transcript entries for run viewer
│   └── build-config.ts   ← Form values → adapterConfig JSON
└── cli/
  └── format-event.ts   ← Terminal output for paperclipai run --watch

Core Database Tables (Drizzle ORM + PostgreSQL)

companies
id, name, description, status, issuePrefix, issueCounter, budgetMonthlyCents, spentMonthlyCents, requireBoardApproval, brandColor
agents
id, companyId, name, role, title, status, reportsTo (self-ref FK), adapterType, adapterConfig (jsonb), runtimeConfig (jsonb), budgetMonthlyCents, permissions
issues
id, companyId, projectId, goalId, parentId (self-ref), title, status, priority, assigneeAgentId, checkoutRunId, executionRunId, issueNumber, identifier
heartbeat_runs
id, agentId, invocationSource, status, startedAt, finishedAt, exitCode, sessionIdBefore/After, logStore, logRef, logBytes, contextSnapshot, retryOfRunId
agent_wakeup_requests
id, agentId, source, triggerDetail, status (queued→claimed→completed), coalescedCount, reason, requestedBy, idempotencyKey
agent_runtime_state
agentId, adapterType, sessionId, stateJson, totalInputTokens, totalOutputTokens, totalCostCents, lastError
goals
id, companyId, title, level, status, parentId (self-ref), ownerAgentId
projects
id, companyId, goalId, name, status, leadAgentId, targetDate, executionWorkspacePolicy
approvals
id, companyId, type, status, requestedByAgentId, resolvedByUserId, payload
cost_events
id, companyId, agentId, runId, inputTokens, outputTokens, costCents, provider, model
activity_log
id, companyId, actorType, actorId, action, entityType, entityId, payload (jsonb)
execution_workspaces
id, companyId, projectId, type (worktree/container), status, path, branchName

Monorepo Package Map (pnpm workspaces)

paperclip/
├── server/                         Express v5 API + heartbeat engine
├── ui/                             React 19 + Vite + TanStack Query
├── cli/                            Commander.js CLI
├── packages/db/                    Drizzle schema + migrations
├── packages/shared/                Shared types (DeploymentMode, etc.)
├── packages/adapter-utils/         Shared adapter helpers
├── packages/adapters/
│   ├── claude-local/               Claude Code CLI
│   ├── codex-local/                OpenAI Codex CLI
│   ├── cursor-local/               Cursor CLI
│   ├── gemini-local/               Gemini CLI
│   ├── opencode-local/             OpenCode CLI
│   ├── pi-local/                   Pi CLI
│   └── openclaw-gateway/           OpenClaw webhook
├── packages/plugins/               Plugin SDK + scaffolder
├── skills/
│   ├── paperclip/                  Core heartbeat protocol skill
│   ├── paperclip-create-agent/     Agent hiring skill
│   ├── paperclip-create-plugin/    Plugin creation skill
│   └── para-memory-files/          PARA memory skill
├── docs/                           Mintlify documentation
├── tests/                          E2E (Playwright)
└── docker/                         Docker assets
The Key Insight

Paperclip is a control plane that never runs AI directly. It's a task queue + org chart + audit log + budget tracker that orchestrates external agent runtimes via adapters. The agents (Claude, Codex, Gemini) do the work — Paperclip tells them when to wake up, what to work on, and tracks what they did.