All documentation

    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-digest template.
    • Target: /functions/v1/send-weekly-digest.
    • Failure symptoms: no digest emails logged in email_send_log for the Monday run; customers reporting they haven't received their weekly summary; the Cron Status page showing the job's last run as failed or its last_run_end missing.
    • 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-engagement afterwards 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_log before 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 winback and/or winback-report template emails based on how long an account has been inactive (see src/lib/winback-logic.ts for the staging logic).
    • Target: /api/public/winback.
    • Failure symptoms: no new rows in email_send_log with template_name = 'winback'; the Email Engagement dashboard (tracks the winback template) 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-expired templates 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_log for 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.server and 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_cron with 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-sync with the shared x-cron-secret header. For a full resync, pass { force: true } with limit/offset to 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.server and 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/leads to 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 without dryRun to 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

    1. Open /admin/cron-status and find the job by name; check last_status, last_return_message, and the recent run history.
    2. If the job failed with an authorisation error, verify the CRON_SECRET used by the calling job matches what the target endpoint expects (x-cron-secret header) — this is a common cause of silent failures after secret rotation.
    3. If the job succeeded but expected emails weren't sent, check /admin/email-engagement and the email_send_log table for the relevant template, and confirm LOVABLE_API_KEY is configured (see email-system.md).
    4. 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_log first for jobs sending to large audiences.