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

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) 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. The system first checks config.heartbeatSchedulerEnabled (defined in 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.

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 (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.

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 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 (lines 2701-2710) allows external systems to trigger wake-ups manually, forwarding to the same heartbeat.wakeup method.

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 (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.

// 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.
  • Multi-layered safety: Execution is suppressed in worktree instances and during database restores via resolveHeartbeatSchedulingSuppression in 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 and validated in server/src/startup-banner.ts, which displays the scheduler status on server boot.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →