Documentation
User & admin guide
Back to Calendar
Admin guide

Cron jobs and ops

What runs on a schedule, where it's defined, how to authenticate manual runs, and how to hand the schedule off to an ops team.

Vercel's built-in cron is the authoritative scheduler. The schedule lives in vercel.json at the repo root and runs automatically on every production deployment. Ops doesn't need to configure anything to keep the schedule alive on a Vercel deployment.

The schedule

JobEndpointScheduleMax durationWhat happens if it misses
Event remindersPOST /api/cron/event-remindersEvery 1 min~30 sUsers miss the 15-minute heads-up push for upcoming meetings
Activity watchesPOST /api/cron/watch-pollEvery 5 min~60 sUsers stop getting "new comment / new file" push notifications
Incremental syncPOST /api/sync/cronEvery 15 min (work hours), hourly (off-hours)~30 s typical, up to 5 minCalendar shows stale data
Full syncPOST /api/sync/cron?mode=fullOnce daily, off-peak~2–5 minCache drift accumulates; mostly harmless short-term
Recurrence materializePOST /api/cron/recurrence-materializeOnce daily, off-peak~30 sNew occurrences of repeat tasks/events don't appear

All five jobs are idempotent. Running one twice in the same minute is safe.

vercel.json is the source of truth. Times are UTC. Edit that file to change any schedule.

Authentication

Every cron route requires a Bearer token:

Authorization: Bearer <CRON_SECRET>

Set CRON_SECRET in Vercel → Project → Settings → Environment Variables. Vercel injects this header automatically when it fires your scheduled routes. Manual callers (curl, the shell script) must add it themselves.

Without the header the endpoint returns 401. If CRON_SECRET is missing from the server env entirely the endpoint returns 500 — it refuses to fail open.

Manual runs

scripts/herbe-cron.sh is a curl wrapper for manual invocations. Use it to:

  • Dry-run a job to see what it would do (dryRun=1 support on event-reminders and watch-poll).
  • Force a sync outside the schedule — useful after restoring a database or fixing a stuck connection.
  • Run jobs from an ops host as an emergency fallback if Vercel cron is ever disabled.

Setup:

export HERBE_BASE_URL=https://calendar.herbe.app
export HERBE_CRON_SECRET=<value from Vercel env>

Usage:

scripts/herbe-cron.sh event-reminders dryrun
scripts/herbe-cron.sh watch-poll
scripts/herbe-cron.sh sync-incremental
scripts/herbe-cron.sh sync-full
scripts/herbe-cron.sh recurrence-materialize

Or directly via curl:

curl -fsS -X POST \
  -H "Authorization: Bearer $HERBE_CRON_SECRET" \
  "https://calendar.herbe.app/api/cron/event-reminders?dryRun=1"

Emergency fallback crontab

If Vercel cron must be taken offline, drop the crons block from vercel.json and install this crontab on any Linux host with the script and env vars configured:

HERBE_BASE_URL=https://calendar.herbe.app
HERBE_CRON_SECRET=<prod secret>
LOG=/var/log/herbe-cron.log

* * * * *        /usr/local/bin/herbe-cron.sh event-reminders      >> $LOG 2>&1
*/5 * * * *      /usr/local/bin/herbe-cron.sh watch-poll           >> $LOG 2>&1
*/15 6-22 * * *  /usr/local/bin/herbe-cron.sh sync-incremental     >> $LOG 2>&1
0 0-5,23 * * *   /usr/local/bin/herbe-cron.sh sync-incremental     >> $LOG 2>&1
0 3 * * *        /usr/local/bin/herbe-cron.sh sync-full            >> $LOG 2>&1
30 3 * * *       /usr/local/bin/herbe-cron.sh recurrence-materialize >> $LOG 2>&1

Monitoring

Check Vercel's Cron tab for green ticks after each scheduled window. For structured log inspection:

vercel logs https://calendar.herbe.app --json \
  | grep -oE '"requestPath":"/api/(cron|sync)/[^"]+"' \
  | sort | uniq -c

A passing run prints OK <job>: {...} to stdout. A failing run exits non-zero and prints to stderr. If you ship logs to a log aggregator, grep for ^[^ ]+ FAIL for alertable lines.

Handing off to an ops team

Share scripts/CRON-HANDOFF.md with whoever runs the ops host. It contains the required env vars, the crontab block, and the expected response shape for each endpoint so they know what "healthy" looks like without reading source.