Skip to content

Cron jobs

TwistCue ships 5 background crons, all @nestjs/schedule based, running inside the API process. They’re the difference between “scheduled data” and “eventual data”.

NameScheduleFileFailure behavior
publish-scheduler* * * * * (every minute)media-sync.cron.tsPer-row try/catch; failed rows flip to status='failed' and retry ≤5 times.
media-sync* * * * * (every minute)media-sync.cron.tsPolls CF Stream for stuck assets; logs and continues on provider errors.
rollups-hourly0 * * * * (top of every hour)rollups.cron.tsRebuilds rollup tables from raw events; failures logged, retried next tick.
free-series-rotation-daily5 0 * * * (00:05 UTC daily)rollups.cron.tsMaterializes today’s free series into feed composer cache.
push-scheduler* * * * * (every minute)push-scheduler.cron.tsPicks up scheduled campaigns; per-campaign try/catch; failed campaigns flip to status='failed'.

All 5 register on API boot; check the log line [NestScheduler] Registered cron '<name>'.

  • Query: publish_schedules WHERE status='pending' AND run_at <= now().
  • Action: performs the workflow transition (publish/unpublish/free_rotation) on the target series/episode inside a transaction.
  • AuditLog: yes, one entry per executed schedule with action='schedule.execute'.
  • Retry: on error, retry_count++ and back off. After 5 failures, row flipped to failed with last_error.
  • Purpose: reconciles asset state with Cloudflare Stream, in case a webhook was lost.
  • Query: media_assets WHERE status IN ('uploading', 'processing') AND created_at < now() - INTERVAL '10 minutes'.
  • Action: hits CF Stream API for each stuck asset, updates status if CF says it’s ready or failed.
  • AuditLog: no (system reconciliation, not admin action).
  • Purpose: rebuilds analytics rollup tables from raw analytics_event and ledger data.
  • Rollups produced:
    • rollup_revenue_daily(day, rail, country)
    • rollup_series_daily(day, series_id)
    • rollup_funnel_daily(day, country)
  • Approach: windowed re-compute of the last 48h of rollups (idempotent: safe to run repeatedly). Older rollups are not re-computed unless explicitly triggered.
  • Manual trigger: POST /v1/admin/analytics/rollups/run.
  • Consequence of missed runs: analytics dashboards can lag by hours until it catches up. Once it runs, all recent windows are correct.
  • Purpose: picks the “today’s free series” per market from free_series_rotations and populates the feed composer’s daily cache.
  • Time: 00:05 UTC (5-minute buffer after midnight to avoid boundary races).
  • Fallback: if no row is set for the date, no free series is offered that day (mobile shows no free banner).
  • Query: push_campaigns WHERE status='scheduled' AND scheduled_at <= now().
  • Action per campaign:
    1. Resolve segment → user_ids (fresh query, not cached).
    2. Apply 3/day cap + category opt-out filter.
    3. Fetch device tokens for deliverable user_ids.
    4. Batch send to APNs / FCM (stubbed in v2: see Integrations status).
    5. Update push_campaigns.metrics with sent/delivered counts.
    6. Flip status to sent (or failed on error).
  • AuditLog: yes, action='push.send'.
  • System → Settings → Health shows last-run time and status per cron.
  • Log lines: [Cron] <name> tick started / <name> tick complete N rows.
  • Cron failures do not page automatically in v2: add alerting via your infra when deploying.

Set TWISTCUE_DISABLE_CRONS=1 in .env to skip cron registration. Useful for smoke tests where you want deterministic behavior.