# Paperclip AI Heartbeat Scheduling and Execution: How Timer-Based Automation Works

> Discover how Paperclip AI heartbeat scheduling and execution powers background automation with timer-based tasks. Learn about watchdog sweeps and maintenance runs.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: deep-dive
- Published: 2026-08-12

---

**Paperclip AI drives background automation through a lightweight heartbeat scheduler that periodically executes timer-based runs, watchdog sweeps, and maintenance tasks while respecting environment-based suppression rules.**

The Paperclip AI platform ([`paperclipai/paperclip`](https://github.com/paperclipai/paperclip)) orchestrates time-sensitive background operations through a dedicated heartbeat scheduling system. This mechanism handles everything from external object refreshes to terminal workspace cleanup, ensuring critical automation runs reliably without interfering with database restores or development worktree instances.

## Configuration and Scheduler Initialization

The heartbeat lifecycle begins during server startup in [`server/src/index.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/index.ts). The system first checks `config.heartbeatSchedulerEnabled` (defined in [`server/src/config.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/config.ts) at lines 332-337), which reads the `HEARTBEAT_SCHEDULER_ENABLED` environment variable. When enabled, the scheduler instantiates the heartbeat service via `heartbeatService(db, { pluginWorkerManager })` and stores it in a global `heartbeat` variable.

The scheduler interval is established using `setInterval(callback, config.heartbeatSchedulerIntervalMs)`, where the interval duration derives from `HEARTBEAT_SCHEDULER_INTERVAL_MS`. Each tick wraps its work in `trackHeartbeatSchedulerWork`, which stores promises in a `Set` to enable graceful shutdown tracking.

```typescript
if (config.heartbeatSchedulerEnabled) {
  const heartbeat = heartbeatService(db, { pluginWorkerManager });
  const callback = async () => {
    if (heartbeatSchedulerStopped) return;
    trackHeartbeatSchedulerWork(
      Promise.all([
        scheduleExternalObjectRefreshSweep(),
        scheduleMergedPullRequestConfirmationSweep(),
        scheduleTerminalWorkspaceSweep(),
        heartbeat.tickTimers(),
      ])
    );
  };
  startHeartbeatSchedulerInterval(callback);
}

```

## Safety Guards and Suppression Logic

Before executing any work, the scheduler evaluates suppression conditions via `resolveHeartbeatSchedulingSuppression` in [`server/src/services/heartbeat.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/heartbeat.ts) (lines 52-66). This function checks environment variables to prevent unsafe execution in specific contexts.

**Worktree instances** are suppressed when `PAPERCLIP_IN_WORKTREE` is truthy, unless the experimental `enableWorktreeRunExecution` override is explicitly armed. **Database restore operations** block scheduling when either `PAPERCLIP_DATABASE_RESTORE_IN_PROGRESS` or `PAPERCLIP_RESTORE_IN_PROGRESS` is set. These guards ensure background tasks do not interfere with development environments or active data recovery.

```typescript
export function resolveHeartbeatSchedulingSuppression(
  env = process.env,
  overrides = {}
): { 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 };
}

```

## Executing Timer-Based Runs

The core automation logic resides in `heartbeat.tickTimers()`, implemented in [`server/src/services/heartbeat.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/heartbeat.ts) at lines 19009-19020. This method scans the `heartbeat_runs` table for rows where `status` equals `"queued"` and `nextRunAt` is less than or equal to the current timestamp.

For each pending run, the system invokes `heartbeat.wakeup(run.id, { source: "timer_tick" })`, which validates the run state, resolves any plan-approval interactions, and schedules the run for execution. Additionally, the public API endpoint `POST /api/heartbeats/:runId/wakeup` defined in [`server/src/routes/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/routes/issues.ts) (lines 2701-2710) allows external systems to trigger wake-ups manually, forwarding to the same `heartbeat.wakeup` method.

```typescript
async function tickTimers(now = new Date()) {
  const pending = await db.select().from(heartbeatRuns)
    .where(and(
      eq(heartbeatRuns.status, "queued"),
      lte(heartbeatRuns.nextRunAt, now)
    ));
  for (const run of pending) {
    await heartbeat.wakeup(run.id, { source: "timer_tick" });
  }
}

```

## Background Sweeps and Maintenance Tasks

Each scheduler tick executes three distinct watchdog sweeps sequentially. These sweeps handle housekeeping tasks that do not require timer-based triggers:

- **`scheduleExternalObjectRefreshSweep()`**: Refreshes due external objects for active companies
- **`scheduleMergedPullRequestConfirmationSweep()`**: Finalizes pending PR confirmations
- **`scheduleTerminalWorkspaceSweep()`**: Archives or cleans up terminal workspaces

These sweeps are defined alongside the scheduler initialization in [`server/src/index.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/index.ts) (lines 47-58). Each sweep call is wrapped in the `trackHeartbeatSchedulerWork` mechanism, ensuring the scheduler can await completion during shutdown.

## Graceful Shutdown and Hot-Restart Support

When the server receives `SIGINT` or `SIGTERM` signals, the heartbeat scheduler initiates a coordinated shutdown sequence. First, `heartbeat.prepareHotRestartShutdown` adopts any still-running runs so they survive process restart. Then, `heartbeat.drainRunningRunsForShutdown` awaits completion of in-flight work.

The `heartbeatSchedulerStopped` boolean flag prevents new ticks from starting during shutdown, while `waitForHeartbeatSchedulerIdle` (which monitors the `Set` of tracked promises) guarantees all scheduled work completes before the interval is cleared and the process exits.

```typescript
// During shutdown
await heartbeat.prepareHotRestartShutdown(signal);
await heartbeat.drainRunningRunsForShutdown(signal);

```

## Summary

- **Configuration-driven initialization**: The scheduler only starts when `HEARTBEAT_SCHEDULER_ENABLED` is true, reading intervals from `HEARTBEAT_SCHEDULER_INTERVAL_MS` in [`server/src/config.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/config.ts).
- **Multi-layered safety**: Execution is suppressed in worktree instances and during database restores via `resolveHeartbeatSchedulingSuppression` in [`server/src/services/heartbeat.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/heartbeat.ts).
- **Timer-based execution**: `tickTimers()` polls the `heartbeat_runs` table and enqueues ready jobs via the `wakeup` method, supporting both automatic ticks and manual API triggers.
- **Comprehensive maintenance**: Each tick runs external object refreshes, PR confirmation sweeps, and terminal workspace cleanup.
- **Graceful degradation**: Hot-restart adoption and drain mechanisms ensure no runs are lost during server restarts or shutdowns.

## Frequently Asked Questions

### What triggers a heartbeat run in Paperclip AI?

Heartbeat runs trigger through two primary paths. The `tickTimers()` method automatically scans the `heartbeat_runs` table on each scheduler interval, enqueueing any rows where `nextRunAt` has passed. Alternatively, external systems can manually trigger execution via the `POST /api/heartbeats/:runId/wakeup` endpoint, which invokes `heartbeat.wakeup()` to validate and schedule the specific run.

### How does Paperclip AI prevent heartbeat execution during database restores?

The `resolveHeartbeatSchedulingSuppression` function checks for `PAPERCLIP_DATABASE_RESTORE_IN_PROGRESS` or `PAPERCLIP_RESTORE_IN_PROGRESS` environment variables. When either is truthy, the function returns `{ suppressed: true, reason: "database_restore_in_progress" }`, causing the scheduler to skip execution for that entire tick cycle.

### What happens to running heartbeats during server shutdown?

During shutdown, the system calls `heartbeat.prepareHotRestartShutdown()` to adopt still-running runs into a state that survives process restart, followed by `heartbeat.drainRunningRunsForShutdown()` to await completion. The `heartbeatSchedulerStopped` flag prevents new work from starting, and `trackHeartbeatSchedulerWork` ensures the process waits for all in-flight promises before exiting.

### Can heartbeat scheduling be disabled or configured?

Yes. Set `HEARTBEAT_SCHEDULER_ENABLED=false` to completely disable the scheduler, or adjust `HEARTBEAT_SCHEDULER_INTERVAL_MS` to change the tick frequency. These values are read from environment variables during startup in [`server/src/config.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/config.ts) and validated in [`server/src/startup-banner.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/startup-banner.ts), which displays the scheduler status on server boot.