# Paperclip AI Routines with Cron Triggers and Catch-Up Policies: Complete Technical Guide

> Master Paperclip AI routines with cron triggers and catch-up policies. Ensure reliable agent execution across time zones and replay missed runs after downtime. Technical guide.

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

---

**Paperclip AI routines combine cron expressions with configurable catch-up policies to ensure autonomous agents execute reliably across time zones, replaying up to 25 missed runs when systems recover from downtime.**

Paperclip is a full-stack control plane that enables teams of AI agents to operate autonomously through scheduled routines. At the heart of this system lies a robust scheduling engine that handles cron triggers, timezone-aware execution, and intelligent catch-up logic when runs are missed. This article examines how the `paperclipai/paperclip` repository implements these mechanisms, from cron parsing to the dispatcher loop that governs routine execution.

## Core Scheduling Components

The scheduling architecture separates parsing, orchestration, and persistence into distinct layers. Each component is testable in isolation and swappable without breaking execution contracts.

- **Cron Parser** ([`server/src/services/cron.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/cron.ts)): Parses standard 5-field cron strings, validates syntax via `validateCron`, and computes next fire times through `nextCronTickInTimeZone`.
- **Routine Service** ([`server/src/services/routines.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/routines.ts)): Orchestrates creation, revision, trigger handling, and the main dispatch loop (lines 2990–3010) that evaluates activity gates and catch-up policies.
- **Database Schema** ([`packages/db/src/schema/routines.ts`](https://github.com/paperclipai/paperclip/blob/main/packages/db/src/schema/routines.ts)): Stores routine definitions, trigger configurations, and revision history in the `routines`, `routineTriggers`, and `routineRevisions` tables.
- **UI Trigger Editor** ([`ui/src/lib/routine-trigger-patch.ts`](https://github.com/paperclipai/paperclip/blob/main/ui/src/lib/routine-trigger-patch.ts)): Provides client-side validation for cron expressions and timezone selection before server submission.

## How Cron Triggers Execute

A cron-triggered routine progresses through five distinct phases from definition to execution.

1. **Trigger Definition**: A routine is created with a trigger of kind `schedule` containing a `cronExpression` (e.g., `0 10 * * *`) and IANA timezone string.
2. **Validation**: The `validateCron` function checks syntax; invalid expressions return user-friendly error messages before persistence.
3. **Next-Run Calculation**: `nextCronTickInTimeZone` walks minute-by-minute in the specified timezone until finding a matching tick (maximum search window of approximately 4 years), storing the result as `nextRunAt`.
4. **Scheduler Tick**: A background worker running every minute queries triggers where `nextRunAt ≤ now`, then evaluates eligibility.
5. **Dispatch**: For eligible triggers, the system creates a routine run record, links it to an Issue, and logs telemetry events.

## Catch-Up Policies and Concurrency Controls

When the scheduler detects missed runs due to downtime or delays, it applies configurable catch-up logic to maintain execution integrity without overwhelming the system.

- **`catchUpPolicy = "replay"`**: The scheduler replays missed executions up to a hard limit of **25 runs** (`MAX_CATCH_UP_RUNS = 25`). This cap prevents runaway loops if a routine was inactive for days. The replay loop (lines 2990–3010 in [`routines.ts`](https://github.com/paperclipai/paperclip/blob/main/routines.ts)) iterates through missed ticks, creating individual run records for each.
- **`catchUpPolicy = "skip"`**: Missed runs are ignored entirely; execution resumes at the next future scheduled tick.

Both policies respect the **concurrency policy** (`single`, `allow`, or `reject`) to control overlapping executions. The `single` policy prevents duplicate runs when catching up, while `allow` permits concurrent execution of missed and current schedules.

## Time-Zone Handling and Activity Gates

Paperclip ensures cron expressions fire at the correct local time regardless of server location through explicit timezone management and activity-based gating.

**Timezone Resolution**: All calculations use `Intl.DateTimeFormat` cached per timezone for performance. The `getZonedMinuteParts` helper converts UTC timestamps into `{minute, hour, day, month, weekday}` objects respecting the trigger's IANA timezone, ensuring `0 9 * * *` fires at 9 AM in the specified zone.

**Activity-Gate Evaluation**: Before firing, `evaluateActivityGate` checks the activity log for qualifying events within the routine's scope (project-wide or global). If no relevant activity exists, the run is suppressed with a "skipped_no_activity" reason, preventing noise on idle routines.

## Implementation Examples

### Creating a Scheduled Routine via API

```typescript
import { fetch } from "node-fetch";

await fetch("http://localhost:3100/api/routines", {
  method: "POST",
  headers: { 
    "Content-Type": "application/json", 
    Authorization: "Bearer <board-token>" 
  },
  body: JSON.stringify({
    title: "Daily sales report",
    description: "Generates a PDF of yesterday's sales.",
    cronExpression: "0 6 * * *",
    timezone: "America/New_York",
    catchUpPolicy: "replay",
    concurrencyPolicy: "single",
    activityGatePolicy: "any",
    activityGateScope: "global",
  }),
});

```

### Computing Next Fire Time Manually

```typescript
import { nextCronTickFromExpression } from "@paperclipai/server/src/services/cron";

const now = new Date();
const next = nextCronTickFromExpression(
  "15 9 * * 1-5", 
  "America/Los_Angeles", 
  now
);
console.log("Next run at:", next?.toISOString());

```

### Simulating Catch-Up Logic

```typescript
// Simulates server downtime recovery with hourly cron
const MAX_CATCH_UP_RUNS = 25;
let missed = 0;
let cursor = lastKnownRunAt;

while (missed < MAX_CATCH_UP_RUNS && cursor <= now) {
  await createRoutineRun({ scheduledAt: cursor });
  missed++;
  cursor = nextCronTickInTimeZone(expr, tz, cursor)!;
}

```

## Summary

- **Cron parsing** occurs in [`server/src/services/cron.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/cron.ts), supporting standard 5-field expressions with timezone-aware calculations via `nextCronTickInTimeZone`.
- **Catch-up policies** limit replay to 25 missed runs (`MAX_CATCH_UP_RUNS`) to prevent system overload while ensuring critical routines recover from downtime.
- **Concurrency controls** (`single`, `allow`, `reject`) govern whether overlapping runs execute during catch-up scenarios.
- **Activity gates** suppress runs when no qualifying activity exists, reducing noise from idle routines.
- **Timezone handling** uses `Intl.DateTimeFormat` with cached formatters to ensure accurate local-time execution across server regions.

## Frequently Asked Questions

### What happens if a Paperclip routine misses more than 25 scheduled runs?

If the `catchUpPolicy` is set to `"replay"` and more than 25 runs are missed, the system caps recovery at 25 executions (`MAX_CATCH_UP_RUNS = 25`) as defined in [`server/src/services/routines.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/routines.ts). Any additional missed runs are discarded to prevent resource exhaustion, and the routine resumes scheduling from the next future tick.

### How does Paperclip handle daylight saving time transitions in cron expressions?

Paperclip stores triggers with explicit IANA timezone strings and uses `getZonedMinuteParts` to convert timestamps into localized calendar components. This ensures cron expressions fire based on wall-clock time in the specified zone, automatically adjusting for DST transitions without duplicate or skipped executions.

### Can multiple instances of the same routine run simultaneously?

This depends on the `concurrencyPolicy` setting. The `"single"` policy prevents overlapping runs by rejecting new executions while one is active. The `"allow"` policy permits concurrent runs, useful for long-running tasks, while `"reject"` simply drops new attempts if any instance is currently executing.

### What is the purpose of the activity gate in Paperclip routines?

The activity gate, evaluated by `evaluateActivityGate`, checks whether relevant activity has occurred since the last run before firing a routine. This prevents unnecessary executions on idle projects, with skipped runs logged under the "skipped_no_activity" telemetry event for observability.