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”.
Overview
Section titled “Overview”| Name | Schedule | File | Failure behavior |
|---|---|---|---|
publish-scheduler | * * * * * (every minute) | media-sync.cron.ts | Per-row try/catch; failed rows flip to status='failed' and retry ≤5 times. |
media-sync | * * * * * (every minute) | media-sync.cron.ts | Polls CF Stream for stuck assets; logs and continues on provider errors. |
rollups-hourly | 0 * * * * (top of every hour) | rollups.cron.ts | Rebuilds rollup tables from raw events; failures logged, retried next tick. |
free-series-rotation-daily | 5 0 * * * (00:05 UTC daily) | rollups.cron.ts | Materializes today’s free series into feed composer cache. |
push-scheduler | * * * * * (every minute) | push-scheduler.cron.ts | Picks 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>'.
publish-scheduler
Section titled “publish-scheduler”- 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 tofailedwithlast_error.
media-sync
Section titled “media-sync”- 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).
rollups-hourly
Section titled “rollups-hourly”- Purpose: rebuilds analytics rollup tables from raw
analytics_eventand 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.
free-series-rotation-daily
Section titled “free-series-rotation-daily”- Purpose: picks the “today’s free series” per market from
free_series_rotationsand 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).
push-scheduler
Section titled “push-scheduler”- Query:
push_campaigns WHERE status='scheduled' AND scheduled_at <= now(). - Action per campaign:
- Resolve segment → user_ids (fresh query, not cached).
- Apply 3/day cap + category opt-out filter.
- Fetch device tokens for deliverable user_ids.
- Batch send to APNs / FCM (stubbed in v2: see Integrations status).
- Update
push_campaigns.metricswith sent/delivered counts. - Flip status to
sent(orfailedon error).
- AuditLog: yes,
action='push.send'.
Monitoring
Section titled “Monitoring”- 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.
Skipping crons in dev
Section titled “Skipping crons in dev”Set TWISTCUE_DISABLE_CRONS=1 in .env to skip cron registration. Useful for smoke tests where you want deterministic behavior.