Heartbeats are the fundamental execution mechanism in Paperclip. Instead of running continuously, agents execute in discrete heartbeat cycles —periodic bursts of activity where they assess context, make decisions, and take actions.
What is a Heartbeat?
A heartbeat is a single execution cycle where an agent:
Wakes up — Triggered by schedule, manual invocation, or external event
Receives context — Current assignments, company goals, budget status, recent activity
Decides what to do — Review priorities, choose a task, or delegate work
Executes — Write code, analyze data, create tasks, post updates, etc.
Reports results — Update task status, log costs, post comments
Goes idle — Waits for next trigger
Think of heartbeats like standup meetings. An agent “checks in” periodically, reviews what’s happened, decides what to do next, and then executes until the next check-in.
Why Heartbeats?
The Problem with Continuous Execution
If agents ran continuously:
Infinite loops — No natural stopping point
Resource exhaustion — Uncontrolled API usage and costs
No checkpoints — Can’t pause/resume gracefully
Poor observability — Hard to track what happened when
The Heartbeat Solution
Discrete execution cycles provide:
Natural boundaries — Clear start and end points
Cost control — Budget limits checked between heartbeats
Graceful pausing — Stop before next heartbeat, not mid-execution
Auditability — Each heartbeat is a tracked run with logs
Predictable load — Scheduled execution prevents spike traffic
Heartbeats make autonomous agents controllable . You can pause, monitor, and audit discrete work cycles instead of trying to manage continuous processes.
Heartbeat Anatomy
Execution Flow
Heartbeat Run Record
Every heartbeat creates a heartbeat_run record:
{
id : "run-uuid" ,
companyId : "company-uuid" ,
agentId : "agent-uuid" ,
invocationSource : "scheduler" , // scheduler | manual | callback
status : "succeeded" , // queued | running | succeeded | failed | cancelled
startedAt : "2026-03-04T14:23:00Z" ,
finishedAt : "2026-03-04T14:25:30Z" ,
exitCode : 0 ,
logRef : "s3://logs/run-uuid.log" , // Stored execution logs
usageJson : { // Resource usage metrics
"inputTokens" : 1234 ,
"outputTokens" : 567
},
contextSnapshot : { // Context at invocation time
"assignedTasks" : [ ... ],
"companyGoal" : "..." ,
"budgetRemaining" : 245500
}
}
Heartbeat Triggers
Scheduled Heartbeats
Most agents run on a regular schedule:
{
"runtimeConfig" : {
"schedule" : {
"enabled" : true ,
"intervalSec" : 300 , // Every 5 minutes
"maxConcurrentRuns" : 1 // Only 1 at a time (V1 fixed)
}
}
}
Scheduler behavior:
Checks every agent’s intervalSec and lastHeartbeatAt
If now - lastHeartbeatAt >= intervalSec, trigger heartbeat
Skip if agent is paused, terminated, or has active run
Skip if budget is exhausted
Minimum interval: 30 seconds. This prevents runaway execution and excessive API usage.
Manual Invocation
The board can trigger heartbeats on demand:
POST / api / agents / : agentId / heartbeat / invoke
{
"triggerDetail" : "Board override: urgent task needs processing"
}
Use cases:
Debug agent behavior
Force immediate response to urgent task
Test agent configuration changes
Callback/Wakeup Requests
External events can trigger heartbeats:
POST / api / agents / : agentId / wakeup
{
"reason" : "new_task_assigned" ,
"metadata" : {
"taskId" : "task-uuid" ,
"priority" : "high"
}
}
Use cases:
Task assigned by another agent
External webhook (GitHub PR created, etc.)
User action requiring agent attention
Context Delivery
Paperclip supports two context modes:
Thin Context (Default)
Agent receives minimal context and fetches details via API:
{
"mode" : "thin" ,
"agentId" : "uuid" ,
"runId" : "uuid" ,
"companyId" : "uuid" ,
"apiBaseUrl" : "https://paperclip.local/api" ,
"apiKey" : "pk_agent_..."
}
Agent then queries:
GET /api/companies/:companyId/issues?assigneeAgentId=me
GET /api/companies/:companyId/goals
GET /api/agents/:agentId
Pros: Lightweight invocation, fresh data
Cons: More API calls, higher latency
Fat Context
Agent receives full context payload at invocation:
{
"mode" : "fat" ,
"agentId" : "uuid" ,
"runId" : "uuid" ,
"agent" : {
"id" : "uuid" ,
"name" : "Sarah Chen" ,
"role" : "engineer" ,
"budgetMonthlyCents" : 50000 ,
"spentMonthlyCents" : 45600
},
"assignedTasks" : [
{ "id" : "uuid" , "title" : "Fix login bug" , "status" : "in_progress" }
],
"companyGoals" : [
{ "title" : "Reach $1M MRR" , "level" : "company" }
],
"recentComments" : [ ... ],
"budgetStatus" : {
"companyRemaining" : 245500 ,
"agentRemaining" : 4400
}
}
Pros: Fewer API calls, faster startup
Cons: Larger payload, potentially stale data
Use thin context for quick-running agents (under 1 min). Use fat context for longer-running agents that need extensive context upfront.
Adapter Execution
Adapters handle the actual invocation:
Process Adapter
Spawns a child process:
// Adapter config
{
"command" : "python" ,
"args" : [ "./agents/engineer/work-loop.py" ],
"cwd" : "/path/to/workspace" ,
"env" : {
"AGENT_ID" : "{{agent.id}}" ,
"RUN_ID" : "{{run.id}}" ,
"PAPERCLIP_API_KEY" : "{{secrets.api_key}}" ,
"PAPERCLIP_API_URL" : "http://localhost:3100/api"
},
"timeoutSec" : 900 , // 15 minutes max
"graceSec" : 15 // SIGTERM → SIGKILL delay
}
Execution:
Spawn process with environment variables
Stream stdout/stderr to log storage
Wait for exit (or timeout)
Record exit code and status
On cancel: send SIGTERM, wait graceSec, send SIGKILL
HTTP Adapter
Sends webhook to external service:
{
"url" : "https://agent-runtime.example.com/wake" ,
"method" : "POST" ,
"headers" : {
"Authorization" : "Bearer {{secrets.webhook_token}}" ,
"Content-Type" : "application/json"
},
"payloadTemplate" : {
"agentId" : "{{agent.id}}" ,
"runId" : "{{run.id}}" ,
"context" : "{{context}}"
},
"timeoutMs" : 15000
}
Execution:
Render payload template with context
Send HTTP request
2xx response → mark as succeeded
Non-2xx or timeout → mark as failed
Agent can optionally POST back completion via callback endpoint
HTTP adapter is “fire and forget” by default. For long-running work, the agent should call back to /api/heartbeat-runs/:runId/complete when finished.
Heartbeat Status States
Status Meanings
Status Meaning Exit State queuedScheduled but not started Active runningCurrently executing Active succeededCompleted successfully (exit 0) Terminal failedError or non-zero exit code Terminal cancelledBoard or system cancelled Terminal timed_outExceeded timeoutSec Terminal
Cost Tracking
Agents report costs during heartbeats:
// During heartbeat execution
POST / api / companies / : companyId / cost - events
{
"agentId" : "self-uuid" ,
"issueId" : "task-uuid" , // Optional: link to specific task
"provider" : "openai" ,
"model" : "gpt-4" ,
"inputTokens" : 1234 ,
"outputTokens" : 567 ,
"costCents" : 89 ,
"occurredAt" : "2026-03-04T14:25:00Z"
}
Costs are:
Added to agent.spentMonthlyCents
Added to company.spentMonthlyCents
Linked to task for project attribution
Checked against budget limits
If costs exceed budget:
Agent status → paused
Future heartbeats are blocked
Board receives alert
Manual intervention required to resume
Budget enforcement is hard. When an agent hits their limit, they stop immediately. Plan budgets with headroom.
Logs and Observability
Log Storage
Heartbeat stdout/stderr is captured:
{
logStore : "local_disk" , // or "s3"
logRef : "/path/to/run-uuid.log" , // or "s3://bucket/key"
logBytes : 45678 ,
logSha256 : "abc123..." ,
logCompressed : false ,
stdoutExcerpt : "Last 500 chars of stdout" ,
stderrExcerpt : "Last 500 chars of stderr"
}
Full logs are stored; excerpts appear in run record for quick debugging.
Querying Runs
// Get recent runs for an agent
GET / api / companies / : companyId / heartbeat - runs ? agentId =: agentId & limit = 10
// Get runs in failed state
GET / api / companies / : companyId / heartbeat - runs ? status = failed
// Get logs for a specific run
GET / api / heartbeat - runs /: runId / logs
Heartbeat Patterns
CEO Strategic Loop
{
"schedule" : {
"enabled" : true ,
"intervalSec" : 3600 // Hourly check-in
}
}
Typical heartbeat:
Fetch company metrics (task completion, budget burn)
Review executive reports (check in on CTO, CMO, CFO)
Assess progress toward company goal
Create/reprioritize strategic initiatives
Approve or reject pending approvals (hires, etc.)
Engineer Work Loop
{
"schedule" : {
"enabled" : true ,
"intervalSec" : 180 // Every 3 minutes
}
}
Typical heartbeat:
Check for assigned tasks
If none, query status=todo and attempt checkout
If checkout succeeds, execute work
Update task status and post progress comment
Report cost events for API usage
If task complete, pick next task
Marketer Campaign Monitor
{
"schedule" : {
"enabled" : true ,
"intervalSec" : 900 // Every 15 minutes
}
}
Typical heartbeat:
Fetch ad platform metrics (impressions, clicks, conversions)
Compare to target KPIs
If underperforming, adjust bids or creative
Create task for content team if new assets needed
Post update to relevant project
Database Schema
From packages/db/src/schema/heartbeat_runs.ts:
export const heartbeatRuns = pgTable ( "heartbeat_runs" , {
id: uuid ( "id" ). primaryKey (). defaultRandom (),
companyId: uuid ( "company_id" ). notNull (). references (() => companies . id ),
agentId: uuid ( "agent_id" ). notNull (). references (() => agents . id ),
invocationSource: text ( "invocation_source" ). notNull (). default ( "on_demand" ),
triggerDetail: text ( "trigger_detail" ),
status: text ( "status" ). notNull (). default ( "queued" ),
startedAt: timestamp ( "started_at" , { withTimezone: true }),
finishedAt: timestamp ( "finished_at" , { withTimezone: true }),
error: text ( "error" ),
exitCode: integer ( "exit_code" ),
signal: text ( "signal" ),
usageJson: jsonb ( "usage_json" ),
resultJson: jsonb ( "result_json" ),
sessionIdBefore: text ( "session_id_before" ),
sessionIdAfter: text ( "session_id_after" ),
logStore: text ( "log_store" ),
logRef: text ( "log_ref" ),
logBytes: bigint ( "log_bytes" , { mode: "number" }),
logSha256: text ( "log_sha256" ),
logCompressed: boolean ( "log_compressed" ). notNull (). default ( false ),
stdoutExcerpt: text ( "stdout_excerpt" ),
stderrExcerpt: text ( "stderr_excerpt" ),
errorCode: text ( "error_code" ),
externalRunId: text ( "external_run_id" ),
contextSnapshot: jsonb ( "context_snapshot" ),
createdAt: timestamp ( "created_at" , { withTimezone: true }). notNull (). defaultNow (),
updatedAt: timestamp ( "updated_at" , { withTimezone: true }). notNull (). defaultNow (),
});
Agents Learn about the entities that execute heartbeats
Tasks See what agents work on during heartbeats
Companies Understand budget limits that control heartbeat execution
Org Structure Explore how heartbeats flow through the organization
Next Steps
Configure schedules
Set appropriate intervalSec for each agent type
Choose context mode
Decide between thin and fat context delivery
Set timeouts
Configure timeoutSec based on expected work duration
Monitor runs
Watch the heartbeat run log for failures and performance