apps/web/app/api/cron/
├── cleanup-old-runs/route.ts # Existing
├── health-check/route.ts # Existing
├── daily-report/route.ts # Existing
└── {new-job-name}/route.ts # New jobs go here
Vercel Cron Registration
When adding a new cron job, update apps/web/vercel.json:
CREATE TABLE IF NOTEXISTS cron_locks (
id TEXT PRIMARY KEY,
locked_at TIMESTAMPTZ NOT NULLDEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
-- Auto-cleanup expired locksCREATE INDEX idx_cron_locks_expires ON cron_locks (expires_at);
Backend Patterns (FastAPI + asyncio)
Startup/Shutdown Lifecycle
from contextlib import asynccontextmanager
from asyncio import create_task, sleep, Task
_background_tasks: list[Task] = []
asyncdefperiodic_cleanup(interval_seconds: int = 3600) -> None:
"""Run cleanup every hour."""whileTrue:
try:
await run_cleanup_job()
except Exception as e:
logger.error("periodic_cleanup_failed", error=str(e))
await sleep(interval_seconds)
@asynccontextmanagerasyncdeflifespan(app: FastAPI):
# Startup: launch background tasks
task = create_task(periodic_cleanup(3600))
_background_tasks.append(task)
yield# Shutdown: cancel all background tasksfor task in _background_tasks:
task.cancel()
APScheduler Integration (When Needed)
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
scheduler = AsyncIOScheduler(timezone="Australia/Brisbane")
scheduler.add_job(
cleanup_old_runs,
CronTrigger(hour=2, minute=0),
id="cleanup_old_runs",
replace_existing=True,
misfire_grace_time=300,
)
scheduler.start()
Cron Expression Reference
Syntax
┌─────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌─────────── day of month (1-31)
│ │ │ ┌─────────── month (1-12)
│ │ │ │ ┌─────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
Common Expressions
Expression
Schedule
Use Case
*/5 * * * *
Every 5 minutes
Health checks
0 * * * *
Every hour
Metrics aggregation
0 2 * * *
Daily 2:00 AM
Data cleanup
0 9 * * *
Daily 9:00 AM
Daily reports
0 9 * * 1-5
Weekdays 9:00 AM
Business reports
0 0 1 * *
Monthly (1st)
Monthly aggregation
0 0 * * 0
Weekly (Sunday)
Weekly cleanup
Timezone Consideration (AEST/AEDT)
Vercel cron runs in UTC. Convert Australian times:
AEST (UTC+10)
AEDT (UTC+11)
UTC
Cron
9:00 AM AEST
10:00 AM AEDT
23:00 (prev day)
0 23 * * *
2:00 AM AEST
3:00 AM AEDT
16:00 (prev day)
0 16 * * *
12:00 PM AEST
1:00 PM AEDT
02:00
0 2 * * *
For the backend (APScheduler), set timezone="Australia/Brisbane" to avoid manual conversion.
Idempotency
Cron jobs may execute more than once (retries, clock drift). Design for idempotency:
Alert after 3+ consecutive failures. Track failure count per job in the database or in-memory store. Log cron_job_alert with consecutive_failures count when threshold is breached.
Anti-Patterns
Anti-Pattern
Why It Fails
Correct Approach
No CRON_SECRET validation
Public endpoint, anyone can trigger
Bearer token auth on every handler
setInterval for critical jobs
Drift, no persistence across restarts
Vercel cron or APScheduler
No overlap protection
Concurrent runs corrupt shared state
Database lock or in-memory flag
Hardcoded UTC offsets
Breaks on AEST/AEDT transition
Use Australia/Brisbane timezone
Non-idempotent inserts
Duplicate records on retry
Upsert with unique constraint
Silent failures
Jobs fail without anyone knowing
Structured logging + alerting
Checklist for New Cron Jobs
Setup
Route handler created in apps/web/app/api/cron/{job-name}/route.ts
Cron entry added to apps/web/vercel.json
CRON_SECRET validation at top of handler
JSDoc with schedule description and cron expression
Safety
Overlap protection implemented (database lock or flag)
Job is idempotent — safe to re-run
Timeout configured (Vercel function timeout or AbortSignal.timeout)
Error handling with structured logging
Observability
cron_job_started log event
cron_job_completed log event with duration_ms
cron_job_failed log event with error details
cron_job_skipped log event for overlap protection
Alerting on consecutive failures
Timezone
Schedule expressed in UTC for Vercel cron
AEST/AEDT conversion documented in JSDoc
Backend uses Australia/Brisbane timezone for APScheduler
Response Format
[AGENT_ACTIVATED]: Cron Scheduler
[PHASE]: {Design | Implementation | Review}
[STATUS]: {in_progress | complete}
{scheduling analysis or implementation guidance}
[NEXT_ACTION]: {what to do next}
Integration Points
Structured Logging
Every cron execution emits structured log events matching structured-logging patterns
correlation_id per execution for tracing across services
Duration tracking in milliseconds
Error Taxonomy
Cron auth failures use AUTH_VALIDATION_MISSING_TOKEN (401)
Job execution failures use SYS_RUNTIME_INTERNAL (500)
Database lock failures use WORKFLOW_CONFLICT_LOCKED (409)