Paperclip AI Routines with Cron Triggers and Catch-Up Policies: Complete Technical Guide
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): Parses standard 5-field cron strings, validates syntax viavalidateCron, and computes next fire times throughnextCronTickInTimeZone. - Routine Service (
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): Stores routine definitions, trigger configurations, and revision history in theroutines,routineTriggers, androutineRevisionstables. - UI Trigger Editor (
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.
- Trigger Definition: A routine is created with a trigger of kind
schedulecontaining acronExpression(e.g.,0 10 * * *) and IANA timezone string. - Validation: The
validateCronfunction checks syntax; invalid expressions return user-friendly error messages before persistence. - Next-Run Calculation:
nextCronTickInTimeZonewalks minute-by-minute in the specified timezone until finding a matching tick (maximum search window of approximately 4 years), storing the result asnextRunAt. - Scheduler Tick: A background worker running every minute queries triggers where
nextRunAt ≤ now, then evaluates eligibility. - 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 inroutines.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
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
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
// 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, supporting standard 5-field expressions with timezone-aware calculations vianextCronTickInTimeZone. - 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.DateTimeFormatwith 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. 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →