Scheduled Jobs
Rank Pilot's background work runs on pg_cron, the Postgres scheduler, which fires HTTP calls into application endpoints (Supabase edge functions and /api/public/* routes) on a fixed UTC schedule. The source-of-truth manifest for the email-related jobs is src/lib/cron/schedules.ts — its SCHEDULED_EMAIL_JOBS array is asserted against in integration tests, so any schedule drift between the database and the code is caught automatically. This file also contains cron-expression helpers (parseCron, nextRunUtc, localTime) used to compute next-run times for display in the admin console.
All times below are UTC, as evaluated by pg_cron. The admin console's Cron Status page (/admin/cron-status) converts these into local wall-clock times where relevant.
Job catalogue
send-weekly-digest
- Schedule:
0 9 * * 1— every Monday at 09:00 UTC. - Purpose: sends the weekly SEO digest email (keyword movement, quick wins, optional ads summary, and a featured blog post block) to active customers. Rendered from the
weekly-digesttemplate. - Target:
/functions/v1/send-weekly-digest. - Failure symptoms: no digest emails logged in
email_send_logfor the Monday run; customers reporting they haven't received their weekly summary; the Cron Status page showing the job's last run asfailedor itslast_run_endmissing. - Re-run: trigger the same function manually (with appropriate authorisation) to resend for the current week, or wait for the next scheduled Monday run. Check
/admin/email-engagementafterwards to confirm sends registered.
send-monthly-rank-reports
- Schedule:
0 9 1 * *— the 1st of each month at 09:00 UTC. - Purpose: sends the monthly rank report email (rendered from
monthly-rank-report) summarising ranking performance over the previous month. - Target:
/functions/v1/send-monthly-rank-reports. - Failure symptoms: no monthly report sends recorded on the 1st; customer queries about a missing report; failed status on Cron Status.
- Re-run: re-invoke the function manually; safe to re-run since it is a monthly snapshot, but check for duplicate sends via
email_send_logbefore doing so for a large customer base.
send-winback-daily
- Schedule:
0 11 * * *— daily at 11:00 UTC. - Purpose: runs the win-back sequence for inactive accounts, sending
winbackand/orwinback-reporttemplate emails based on how long an account has been inactive (seesrc/lib/winback-logic.tsfor the staging logic). - Target:
/api/public/winback. - Failure symptoms: no new rows in
email_send_logwithtemplate_name = 'winback'; the Email Engagement dashboard (tracks thewinbacktemplate) showing no new sends for a day or more. - Re-run: call the endpoint again for the current day; it is designed to be idempotent per account/day, so re-running should not duplicate sends to the same recipient.
trial-nudge-emails-daily
- Schedule:
0 9 * * *— daily at 09:00 UTC. - Purpose: sends trial-ending nudges (
trial-ending-3day,trial-ending-1day,trial-expiredtemplates depending on how close the trial is to ending). - Target:
/functions/v1/trial-nudge-emails. - Failure symptoms: customers on trial not receiving reminder emails before expiry; increased confused cancellations at trial end; missing entries in
email_send_logfor the relevant templates. - Re-run: re-invoke the function for the current day; confirm via Users & Signups (
/admin/users) that affected trial accounts are in the expected state before and after re-running.
nurture-incomplete-signups
- Schedule:
0 10 * * *— daily at 10:00 UTC. - Purpose: nurtures accounts that started but did not complete signup (e.g. incomplete verification), prompting them to finish setup. Related logic lives under
supabase/functions/nurture-incomplete-signups. - Target:
/functions/v1/nurture-incomplete-signups. - Failure symptoms: incomplete-signup accounts sitting untouched for multiple days; no corresponding activity in send logs.
- Re-run: re-invoke the function manually for the current day.
mailchimp-plan-sync-daily
- Schedule:
30 5 * * *— daily at 05:30 UTC. - Purpose: syncs each customer's trial/plan state into Mailchimp as tags, keeping marketing segmentation aligned with billing reality. Implemented in
src/lib/mailchimpPlanSync.serverand called via/api/public/mailchimp-plan-sync. Does not send email itself. - Target:
/api/public/mailchimp-plan-sync. - Failure symptoms: Mailchimp segments/tags falling out of step with actual plan or trial status (e.g. a converted customer still tagged as "trial"); marketing campaigns targeting the wrong audience.
- How it's called: nightly by
pg_cronwith an empty body (a bounded sweep); immediately after a trial starts or a plan changes, with{ userIds: [...] }; or as a one-off backfill with{ force: true, limit, offset }. - Re-run: call
/api/public/mailchimp-plan-syncwith the sharedx-cron-secretheader. For a full resync, pass{ force: true }withlimit/offsetto page through all accounts safely.
mailchimp-blog-campaign-weekly
- Schedule:
0 10 * * 3— every Wednesday at 10:00 UTC. - Purpose: sends the weekly blog marketing campaign to leads (non-customers) via Mailchimp, implemented in
src/lib/mailchimpBlogCampaign.serverand called via/api/public/mailchimp-blog-campaign. Does not use the transactional Lovable email pipeline — it drives a Mailchimp campaign send instead. - Target:
/api/public/mailchimp-blog-campaign. - Failure symptoms: no new Mailchimp campaign created/sent on Wednesday; leads not receiving the weekly blog email; check
/admin/leadsto confirm there is an eligible lead list for the campaign to send to. - Re-run: call the endpoint with
{ dryRun: true }first to verify the campaign content and audience without sending, then call again withoutdryRunto send for real.
Related (non-manifest) jobs
The admin Cron Status page also tracks jobs that are not part of the email manifest but run on the same pg_cron infrastructure, including:
daily-rank-check— the daily keyword rank-tracking sweep that other digest/report jobs depend on.expire-trials-hourly— expires trials that have passed their end date, hourly.
These are labelled in JOB_LABELS on the Cron Status page and should be checked alongside the email jobs above, since a failure here (e.g. rank data not refreshing) can cascade into misleading digest or report emails later in the week.
General failure triage
- Open
/admin/cron-statusand find the job by name; checklast_status,last_return_message, and the recent run history. - If the job failed with an authorisation error, verify the
CRON_SECRETused by the calling job matches what the target endpoint expects (x-cron-secretheader) — this is a common cause of silent failures after secret rotation. - If the job succeeded but expected emails weren't sent, check
/admin/email-engagementand theemail_send_logtable for the relevant template, and confirmLOVABLE_API_KEYis configured (seeemail-system.md). - Re-run jobs by invoking their target endpoint directly with the correct secret header; most jobs are written to be safe to re-run (idempotent per recipient/day), but always check
email_send_logfirst for jobs sending to large audiences.