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 run together as one group on a quarter-hour cadence on purpose: their database work batches 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”Six schedules (SC-1688). Four of them are groups: each runs its steps in order, every step through its own advisory lock, retry, heartbeat and Sentry name, and a step that fails, times out or finds its lock taken never stops the steps after it. A re-attempt of the same run skips the steps that already succeeded. The steps are listed under Grouped steps.
Grouped steps
Section titled “Grouped steps”Each step is a scheduled job of its own: its own lock, heartbeat and Sentry name, and the admin’s “Run now” runs it alone. It has no cron; its group owns the schedule.
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. Nothing consumes that
queue: the dlq-depth-probe job alerts once for each new entry and removes it
14 days after it arrived.
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.