# How Paperclip's Heartbeat System Triggers and Manages Agent Execution

> Discover how Paperclip's heartbeat system triggers agent execution via its wakeup API, scheduling checks, and a multi-stage pipeline for seamless workflow management.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: internals
- Published: 2026-08-16

---

**Paperclip's heartbeat system converts scheduled "runs" into live agent execution through a centralized `wakeup` API, scheduling suppression checks, and a multi-stage execution pipeline that handles workspace preparation, adapter invocation, and result persistence.**

The heartbeat subsystem in [paperclipai/paperclip](https://github.com/paperclipai/paperclip) serves as the bridge between user intent and autonomous agent action. Whether triggered by a UI interaction, CLI command, or automatic timer, every execution flows through [`server/src/services/heartbeat.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/heartbeat.ts) — a server-side service that coordinates queuing, scheduling, and the full agent lifecycle.

## The Wakeup API: Entry Point for All Execution

Every heartbeat run begins with a call to `heartbeat.wakeup`. This method is the single gateway through which three different trigger sources enqueue work:

| Trigger Source | Path to Wakeup | Implementation Location |
|--------------|----------------|------------------------|
| UI (comments, approvals, timers) | `POST /api/heartbeats/wakeup` → [`routes/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/routes/issues.ts) | `server/src/routes/issues.ts:2720-2725` |
| CLI command | Direct service invocation | [`cli/src/commands/heartbeat-run.ts`](https://github.com/paperclipai/paperclip/blob/main/cli/src/commands/heartbeat-run.ts) |
| Scheduled timer events | `tickTimers()` → `startNextQueuedRunForAgent` | `heartbeatService` (~line 7000) |

The `wakeup` method performs two critical tasks. First, it inserts a row into `heartbeat_runs` with status `queued`. Second, it registers a promise in `activeWakeupPromises` (lines 777-795) so tests and graceful shutdown can await visibility of the new run. A companion set, `activeRunExecutionPromises`, tracks when execution actually begins.

```typescript
// ui/src/api/heartbeats.ts
export const heartbeatsApi = {
  async wakeup(companyId: string, agentId: string, payload: WakeupPayload) {
    return fetch(`/api/heartbeats/${companyId}/${agentId}/wakeup`, {
      method: "POST",
      body: JSON.stringify(payload),
      headers: { "Content-Type": "application/json" },
    }).then(r => r.json());
  },
};

// Example usage
await heartbeatsApi.wakeup("company-123", "agent-abc", {
  issueId: "ISSUE-42",
  reason: "issue_commented",
});

```

## Scheduling Suppression and Safety Guards

Before any queued run begins execution, `heartbeatService` checks whether the environment permits scheduling. The `resolveHeartbeatSchedulingSuppression` function (lines 6648-6662) evaluates two suppression conditions:

```typescript
export function resolveHeartbeatSchedulingSuppression(
  env: Record<string, string | undefined> = process.env,
  overrides: { allowWorktreeRunExecution?: boolean } = {}
): { suppressed: boolean; reason: "worktree_instance" | "database_restore_in_progress" | null } {
  if (isTruthyRuntimeEnvValue(env.PAPERCLIP_IN_WORKTREE) && !overrides.allowWorktreeRunExecution) {
    return { suppressed: true, reason: "worktree_instance" };
  }
  if (isTruthyRuntimeEnvValue(env.PAPERCLIP_DATABASE_RESTORE_IN_PROGRESS) ||
      isTruthyRuntimeEnvValue(env.PAPERCLIP_RESTORE_IN_PROGRESS)) {
    return { suppressed: true, reason: "database_restore_in_progress" };
  }
  return { suppressed: false, reason: null };
}

```

**Worktree instances** suppress execution unless `enableWorktreeRunExecution` is enabled (cached 3 seconds, lines 6778-6800). **Database restore operations** unconditionally block scheduling to prevent data corruption. The background `tickTimers` loop only dequeues when suppression returns `{ suppressed: false }`.

## The Five-Stage Execution Pipeline

Once cleared through suppression checks, `heartbeatService` executes a standardized pipeline:

### 1. Configuration Resolution

`resolveExecutionRunAdapterConfig` (lines 9970-10125) aggregates the agent's runtime configuration, secret bindings, environment variables, and project-level environment into a unified execution context.

### 2. Workspace Preparation

Three coordinated calls ensure a valid working environment:
- `ensurePersistedExecutionWorkspaceAvailable`
- `realizeExecutionWorkspace`
- `ensureGitWorktreeBranchCoherent`

These are imported at lines 1337-1384 and handle directory creation, git state verification, and branch synchronization.

### 3. Adapter Invocation

`getServerAdapter` returns the concrete adapter (`claude_local`, `codex_local`, etc.). The adapter's `execute` method receives the prepared configuration and streams output to `runLogStore`. This is where the actual LLM interaction occurs.

### 4. Event Recording

Throughout execution, the service writes structured events to `heartbeat_run_events` with kinds like `heartbeat.run.progress`, `heartbeat.run.queued`, and `heartbeat.run.completed`. The `publishLiveEvent` helper broadcasts these via websocket to UI listeners.

### 5. Termination and Result Handling

On completion, `mergeHeartbeatRunResultJson` combines partial results and `buildHeartbeatRunIssueComment` posts a final summary comment. Errors are classified — transient upstream failures, max-turn exhaustion, etc. — and may trigger automatic retry with `scheduled_retry` status.

## Concurrency Control and Runtime Limits

**Per-agent and per-company limits** are enforced through `budgetService` (lines 838-840) using the agent's `heartbeat.enabled` and `heartbeat.intervalSec` configuration. The system defaults to `HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT` (line 47).

When a run requires a workspace currently held by another execution, the service throws `WorkspaceBusyDeferral` (lines 48-55) and schedules retry with exponential backoff via `computeWorkspaceBusyRetryDelayMs` (lines 821-825).

## Result Persistence and Cleanup

After execution terminates, [`heartbeat-run-summary.ts`](https://github.com/paperclipai/paperclip/blob/main/heartbeat-run-summary.ts) handles result formatting. It builds a concise comment (max 4096 characters) respecting `HEARTBEAT_RUN_RESULT_OUTPUT_MAX_CHARS` and related constants (lines 7-11). Each run operates within a scratch environment created by [`run-scratch.ts`](https://github.com/paperclipai/paperclip/blob/main/run-scratch.ts), which `cleanupHeartbeatRunScratch` (line 84) removes after completion.

## Service Integration Architecture

The heartbeat service coordinates with multiple supporting services defined by imports at lines 0-70:

| Service | Responsibility |
|---------|--------------|
| `environmentService` / `environmentRuntimeService` | Secret resolution and container provisioning |
| `executionWorkspaceService` | Workspace lifecycle management |
| `budgetService` | Concurrency quota enforcement |
| `recoveryService` | Failed run detection and retry |
| `taskWatchdogService` | Zombie run prevention |
| `issueService` / `issueTreeControlService` | Issue state updates and comment posting |
| `logActivity` / `publishLiveEvent` | Audit logging and real-time UI updates |

The **active-run-executions** set (lines 777-779) and **active-run-execution-promises** set (lines 780-785) together provide the hooks needed for test synchronization and graceful shutdown — ensuring no runs disappear into unobservable background execution.

## CLI and Programmatic Invocation

Beyond UI triggers, developers can invoke heartbeat execution directly:

```typescript
// cli/src/commands/heartbeat-run.ts
export async function runHeartbeat(args) {
  const db = await getDb();
  const heartbeat = heartbeatService(db);
  await heartbeat.wakeup({
    companyId: args.company,
    agentId: args.agent,
    issueId: args.issue,
    reason: "manual_trigger",
  });
}

```

The background scheduler polls for queued work in a continuous loop:

```typescript
// Simplified from heartbeatService implementation
async function tickTimers() {
  const { suppressed } = await getSchedulingSuppression();
  if (suppressed) return;
  const queued = await db.select().from(heartbeatRuns)
                       .where(eq(heartbeatRuns.status, "queued"))
                       .limit(10);
  for (const run of queued) {
    await startNextQueuedRunForAgent(run);
  }
}

```

## Summary

- **Single entry point**: All agent execution flows through `heartbeat.wakeup`, creating a `queued` row in `heartbeat_runs`
- **Safety-first scheduling**: Worktree and database-restore suppression prevents execution in unsafe environments
- **Five-stage pipeline**: Configuration → workspace preparation → adapter execution → event recording → result handling
- **Observable execution**: Promise sets track active runs for testing and shutdown; events stream to UI in real-time
- **Resilient retries**: `WorkspaceBusyDeferral` and classified errors trigger appropriate backoff and retry strategies
- **Tight service integration**: Budget, recovery, and watchdog services collaborate to ensure reliable, bounded execution

## Frequently Asked Questions

### What happens if two wakeup calls target the same agent simultaneously?

Per-agent concurrency limits enforced by `budgetService` (lines 838-840) ensure only `HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT` runs execute concurrently. Additional wakeups create `queued` rows that `tickTimers` dequeues when capacity becomes available.

### How does Paperclip prevent heartbeat execution during database maintenance?

The `resolveHeartbeatSchedulingSuppression` function checks `PAPERCLIP_DATABASE_RESTORE_IN_PROGRESS` and `PAPERCLIP_RESTORE_IN_PROGRESS` environment variables (lines 6656-6659). When either is truthy, all scheduling halts until the flags clear.

### Can I trigger heartbeat execution from outside the Paperclip UI?

Yes — the `paperclipai heartbeat-run` CLI command ([`cli/src/commands/heartbeat-run.ts`](https://github.com/paperclipai/paperclip/blob/main/cli/src/commands/heartbeat-run.ts)) and the HTTP endpoint `POST /api/heartbeats/:companyId/:agentId/wakeup` ([`ui/src/api/heartbeats.ts`](https://github.com/paperclipai/paperclip/blob/main/ui/src/api/heartbeats.ts)) both provide direct access to the same `wakeup` method.

### What determines which agent adapter executes a heartbeat run?

`resolveExecutionRunAdapterConfig` (lines 9970-10125) resolves the agent's configured adapter type (`claude_local`, `codex_local`, etc.), then `getServerAdapter` instantiates the appropriate implementation. The adapter's `execute` method receives the fully resolved configuration including secrets and environment variables.