Skip to content
DDelTech MUNDocs

Cron jobs

Three scheduled routes, how they authenticate and how they stay idempotent.

Three ordinary route handlers, called on a schedule by .github/workflows/cron.yml.

RouteSchedule (UTC)Does
/api/cron/payment-reminder0 3 * * *Emails allotted, unpaid delegates
/api/cron/gform-sync30 3 * * *Re-pulls every configured sheet through intake
/api/cron/media-sweep0 4 * * *Clears orphaned pending and failed media assets

Off unless switched on

The workflow does nothing unless the repository variable CRON_ENABLED is true. It exists so a new scheduler can be merged before the old one is turned off.

Careful

If reminders are not arriving, check this first. A disabled schedule fails silently: the workflow run shows as skipped, and no route is called.

gh variable list
gh run list --workflow cron.yml --limit 5

Only production is scheduled. Staging's routes exist but are never called on a timer.

Authentication

Every route requires Authorization: Bearer $CRON_SECRET. Without a match it rejects the call. Production and staging have different secrets, so staging cannot trigger production's mailout.

Payment reminder

The most defensive of the three:

  • a run lock, so two invocations cannot overlap;
  • eighty messages per run, so a backlog cannot become a send burst;
  • eight-way concurrency;
  • twenty-four hour deduplication per delegate, checked against EmailLog;
  • a no-op when payments are disabled, so a free event sends nothing.

Google Form sync

Re-pulls each configured sheet through createDelegateFromRow(). Identity is the email address, so re-processing cannot create duplicates. A self-heal for webhook outages, not the primary route. See the intake pipeline.

Media sweep

Removes MediaAsset rows stuck in PENDING or FAILED, the residue of uploads that started and never finished. See media and uploads.

Running one by hand

From the Actions tab: Cron (production) → Run workflow → pick the job. This runs regardless of the schedule but still respects CRON_ENABLED.

Locally, against your own dev server:

curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/media-sweep

Set EMAIL_REDIRECT_TO first if you run the payment reminder, or it mails whoever is in your database.

Adding a job

  1. Create the route under src/app/api/cron/.

  2. Check the bearer token before anything else.

  3. Make it idempotent. It will be retried, and it may run twice.

  4. Cap the work per invocation. The whole request has five minutes in the workflow's curl.

  5. Add a schedule line and a case entry in cron.yml.