Create and manage background jobs with graphile-worker using makeJob and makeCronJob. Use when adding new background jobs, cron jobs, fan-out patterns, enqueuing work, or working with the worker infrastructure.
Create and manage background jobs with graphile-worker using makeJob and makeCronJob. Use when adding new background jobs, cron jobs, fan-out patterns, enqueuing work, or working with the worker infrastructure.
Background Jobs
Background jobs use graphile-worker via makeJob and makeCronJob from ~/framework/worker.server. The app registers its jobs in app/business/jobs.server.ts and runs them in a separate process (run-worker.ts).
graphile-worker mutates its own graphile_worker schema internally — that is fine; it is third-party-owned. Job handlers themselves write to the application schema, so they follow {{write-discipline}} (load the database-design skill).
Framework API
makeJob(jobName, run, options?) — On-Demand Job
Creates a job that is enqueued explicitly from business logic.
Returns everything from makeJob plus cronItem with the schedule. The options parameter works the same as makeJob — priority is applied both to the cron item and to any manual enqueue calls.
Naming Convention
Job names must start with a verb. Both the variable name and the job name string (first argument to makeJob/makeCronJob) must match.
const processItem = makeJob(
'processItem',
async ({ itemId, intakeId }: { itemId: string; intakeId: string }) => {
// Idempotency check first: query the database for evidence the work// already happened, and return early when it did (see Idempotency below)// process...
},
)
Both jobs must be registered in jobs.server.ts.
Error Tracking
Track which step failed by using a mutable step variable. Wrap the success path in a transaction (see Transactions section below) and record failures outside the transaction so they always persist. Always re-throw the error after recording it — graphile-worker only retries jobs that throw, so swallowing errors silently marks the job as succeeded:
When a job runs multiple database queries, wrap them in a transaction for atomicity:
awaitdb()
.transaction()
.execute(async (trx) => {
// Use trx instead of db() for all queries inside the callbackconst record = await trx.selectFrom('items').select('id').where('id', '=', itemId).executeTakeFirstOrThrow()
await trx.insertInto('itemCompletions').values({ itemId: record.id }).execute()
})
Rules:
Use trx (not db()) for all queries inside the transaction callback
Transactions auto-rollback on exceptions — no manual rollback needed
Keep failure recording OUTSIDE the transaction so it persists after rollback (see Error Tracking pattern above)
When creating records then enqueuing a child job, return data from the transaction and enqueue AFTER it commits — this prevents enqueuing jobs that reference uncommitted records
Single-query jobs (e.g. one SELECT + an email side effect) do not need transactions
Payload Design
Keep payloads minimal — only IDs and essential data
Do not include data that is only useful for debugging
Use inline typed object parameters (no separate type declaration needed)
// Good: minimal payload with only what's neededconst processItem = makeJob(
'processItem',
async ({ itemId, intakeId }: { itemId: string; intakeId: string }) => {
// ...
},
)
// Bad: including extra data not used by the jobconst processItem = makeJob(
'processItem',
async ({ itemId, intakeId, originalName, createdBy }: { ... }) => {
// originalName and createdBy are never used
},
)
Idempotency
Jobs may be retried by graphile-worker. Always check if the work has already been done before doing it: derive "already done" from the database itself — query for the evidence the work leaves behind, keyed by the payload's ids — and return early when it is found. The check runs as the handler's first step, before any side effect.
For fan-out patterns, the parent cron job should also prevent duplicate enqueuing — typically by checking a database table or using unique constraints.
Testing
Testing a Job's Run Function
Call .run() directly with the payload and an empty JobHelpers: