add-job
Add a background job, queue, scheduled task or domain-event consumer, in TypeScript or in the Python service. Use when work should run outside the request (sending, processing, syncing, cleanup) or wh
SKILL.md.claude/skills/add-job/SKILL.md
GitHubAdd a job or event consumer
Every queue and payload is declared once, in packages/jobs/src/queues.ts. Producers can
only enqueue what type-checks there, and consumers re-validate with parseJob, because
producer and consumer can be different versions during a rollout.
A job on a queue
- In
queues.ts, add the job (a zod payload) to an existing queue, or a new queue with itsoptions(retries,removeOnComplete,removeOnFail). Payloads carry ids, not copies of rows. Work that must never be lost goes through the outbox (below), not a direct job. - Produce:
createProducer("<queue>", redis), thenadd("<job>", payload, { jobId }).jobIdis required: a UUIDv7 or a natural key, so a retried request enqueues once. - Consume in the owning service:
BullModule.registerQueue({ name, prefix: queuePrefix(name) })in its module, and a@Processor(name, { concurrency, prefix: queuePrefix(name) })class extendingJobProcessor(@repo/nest-common, neverWorkerHostdirectly: it logs failed jobs) whoseprocess(job: UncheckedJob)starts withparseJob("<queue>", "<job>", job.data)(orjobNamefirst, for a queue with several jobs) (seeapps/worker/src/files/). The handler must be idempotent: jobs are delivered at least once. - Scale: add the queue to the consuming service's
keda.queuesindeploy/charts/stack/values.yaml. A queue KEDA scales must not use job priorities. - Scheduled work: a job on the
maintenancequeue and a cron line inSCHEDULESinapps/worker/src/maintenance/maintenance.processor.ts(upserted at boot, runs once whatever the replica count).
A consumer of domain events
Add a queue with jobs: { event: eventEnvelope } and a filter in eventSubscribers in
queues.ts; the outbox relay (apps/worker) copies matching events into it with
jobId = event id. Consumers must be idempotent and not depend on order: the current
ones dedupe on a key of their own (the audit log on the event id, webhook deliveries on
endpoint and event, notifications on their delivery log key). A consumer without one
inserts (event_id, consumer) into its schema's processed_event in the same
transaction as its effect and skips when the row already exists. New events are declared in packages/contracts/src/events.ts.
Python producer or consumer (apps/ai)
- In
packages/jobs/scripts/export-schemas.ts, add the payload toschemasand the queue toshared. bun run gen: writespackages/jobs/generated/and the Pydantic models inapps/ai/app/contracts/, plus the queue's prefix and options.- Use the generated model and
queue_optionsinapps/ai/app/queues.py; the worker loop isapps/ai/app/worker.py(run locally withbun run --cwd apps/ai worker).
Check
bun run test (payload schemas in packages/jobs/src/producer.test.ts), then
bun run test:integration: the consuming service's suite in its test/ folder drives
the queue against real Redis.
Finish
The verify skill, then the reviewer agent (idempotency, jobId, parseJob, no
awaits inside a transaction). A Python producer or consumer: the python-reviewer agent
too.