Job catalogue
Every async job runs through the same BullMQ queue (scani-jobs),
consumed by apps/backend/worker. Wire names live in
packages/business/jobs/src/job-names.ts; descriptors in
packages/business/jobs/src/scheduled-jobs/ (for repeatable jobs)
or packages/business/jobs/src/user-jobs/ (for user-initiated
jobs); processors in apps/backend/worker/src/processors/.
Scheduled jobs use the advisory-lock wrapper — two overlapping fires of the same name silently no-op rather than race.
The reconcilers and probes all run on the same quarter-hour cadence on purpose: aligning them means their advisory locks batch into one wake, so the database can scale to zero between runs instead of being nudged awake four times an hour.
Scheduled jobs
Section titled “Scheduled jobs”| alert-sweep | Daily, 09:00 UTC (0 9 * * *) | Evaluate the named alert rules and email each affected account at most once per fault. Today there is one rule, integration-stale: an active, credentialed integration whose accounts have not synced for ALERT_STALE_SYNC_HOURS (default 24), or which has never produced an account at all. Same signal as stale-sync-probe above, at a far looser threshold and pointed at the USER rather than at Sentry — 3h is two missed cycles and the right moment to page us, and the wrong moment to mail somebody about a blip. One letter per account however many connections it names, and it names only the ones this run claimed, never everything still broken. alert_deliveries is what makes that true across a BullMQ retry; a row is deleted when the integration syncs again, so a fault that recurs alerts a second time. Unverified addresses and accounts that opted out are never claimed for. Every letter carries a one-click, no-login unsubscribe (GET /e/a/:token on the api) that is SEPARATE from the digest’s. Needs FRONTEND_URL + BACKEND_URL on the worker; without them the job logs a refusal on every fire and sends nothing. |
Scheduled jobs — declared but not registered
Section titled “Scheduled jobs — declared but not registered”A descriptor can exist in packages/business/jobs/src/scheduled-jobs/
without being listed in SCHEDULED_JOB_DESCRIPTORS, and then the worker
never registers it: the jobs below do not run. They are listed here
because the file is in the tree and a reader who finds it deserves to know
which half is missing — not because they are live. The table above is the
list of live jobs.
A descriptor belongs here only for as long as its processor is unwritten —
payment-due-reminder was one, and moved to the live table above when
PaymentDueReminderProcessor landed in the same commit that registered it
(SC-226).
demo-reset is the one entry that is deliberately permanent, and it is
the exception to the paragraph above: its processor exists and is registered,
but the worker arms the schedule only when SCANI_DEMO_MODE=1 — and in that
case it arms nothing else, because every other job here damages the seeded
demo dataset. So on the deployment you are reading about it does not run, and
on a demo instance it is the only job that does.
The heading stays even when empty, because scripts/check-docs.ts reads both
tables and fails if either is missing — and a page that silently loses the
distinction is how a job that never runs gets read as one that does.
User-initiated jobs
Section titled “User-initiated jobs”Enqueued by the api in response to a user action. They use a stable per-user job ID so the user can see “in flight” status in the SPA.
Retry policies
Section titled “Retry policies”Defined in packages/business/jobs/src/retry-policies.ts:
DLQ (dead-letter queue)
Section titled “DLQ (dead-letter queue)”Jobs that exhaust their retries land in scani-dlq. The
dlq-depth-probe job alarms when depth grows. Operators replay
via the HMAC-gated jobs.dlqReplay endpoint on the api.
Adding a job
Section titled “Adding a job”See Adding a scheduled job for the three-place change required.
See also
Section titled “See also”- Why BullMQ + Postgres advisory locks
- Adding a scheduled job
- Portfolio value rollup — what the nightly chain produces.
- Observability — which jobs emit log-based metrics.