Scheduled Jobs (Cron)
The EGAC platform runs four scheduled jobs via Cloudflare Workers cron triggers. Each job is both a cron handler (fired by Cloudflare’s scheduler) and a standard HTTP handler (callable via POST with a CRON_SECRET bearer token). This dual design makes every job testable and replay-able without waiting for the scheduler.
Cron schedule
| Route | Schedule | Description |
|---|---|---|
/api/cron/expire-invites | 0 9 * * * | Daily 09:00 UTC — expire stale booking invites |
/api/cron/send-reminders | 0 18 * * 2 | Tuesday 18:00 UTC — send 7-day session reminders |
/api/cron/retry-invites | */30 * * * * | Every 30 minutes — retry failed invite sends |
/api/cron/academy-rollover | 0 9 1 * * | 1st of month 09:00 UTC — season rollover |
Authentication
All cron routes require Authorization: Bearer <CRON_SECRET>. The CRON_SECRET is a Cloudflare secret (set via wrangler secret put CRON_SECRET).
The Cloudflare scheduler calls them by constructing a synthetic POST request inside the Worker’s scheduled() handler:
async scheduled(event, env, ctx) {
const headers = {
Authorization: `Bearer ${env.CRON_SECRET}`,
'Content-Type': 'application/json',
};
if (event.cron === '0 9 * * *') {
ctx.waitUntil(handleRequest(
new Request('https://worker.internal/api/cron/expire-invites',
{ method: 'POST', headers }), env));
}
// ... other cron mappings
}
expire-invites — daily 09:00 UTC
Marks sent invites older than 14 days as expired. Idempotent: already-expired invites are not in the sent status bucket, so repeated runs are safe.
send-reminders — Tuesday 18:00 UTC
Sends reminder emails to all parents with confirmed bookings exactly 7 days from the run date. Since the job runs every Tuesday at 18:00, it targets sessions happening the following Tuesday.
Idempotency: before sending, the job checks enquiry.events for a prior reminder_sent event with the same booking_id. If found, the reminder is skipped.
retry-invites — every 30 minutes
Retries pending booking invite emails that failed to send. An invite is retried if status = 'pending' and send_attempts < 3.
After 3 total failures, the invite is marked as failed and no further retries occur. The club is expected to handle these manually (admin can resend from the enquiry detail page).
academy-rollover — 1st of month 09:00 UTC
Closes expired Academy seasons and moves unplaced waitlist entries into the next upcoming season. Full details are in the Academy Waitlist documentation.
Manual triggering
Any cron job can be triggered manually for testing or emergency replay:
curl -X POST https://your-site.pages.dev/api/cron/expire-invites \
-H "Authorization: Bearer <CRON_SECRET>"
Or in local dev (where dev token works):
curl -X POST http://localhost:4321/api/cron/retry-invites \
-H "Authorization: Bearer dev"
Monitoring
Each job logs a completion summary as structured JSON:
{ "level": "info", "event": "expire_invites_complete", "expired": 3 }
{ "level": "info", "event": "send_reminders_complete", "sent": 12, "skipped": 1 }
{ "level": "info", "event": "retry_invites_complete", "retried": 2, "failed": 0 }
{ "level": "info", "event": "academy_rollover_complete", "rolledOver": 18, "seasonsClosed": 1 }
Errors within individual items are logged at error level with the relevant IDs, so failures are visible in Cloudflare Pages logs without stopping the rest of the batch.