Job Processing Modes
Atomic Mode
Section titled “Atomic Mode”Most jobs don’t need prepare. Call complete directly and you get atomic mode automatically — one transaction for all reads and writes:
'reserve-inventory': { attemptHandler: async ({ job, complete }) => { return complete(async ({ finish, txSql }) => { const [item] = await txSql`SELECT stock FROM items WHERE id = ${job.input.id}`; if (item.stock < 1) throw new Error("Out of stock"); await txSql`UPDATE items SET stock = stock - 1 WHERE id = ${job.input.id}`; return finish({ output: { reserved: true } }); }); },}This is the default path. If you’re not sure which mode to use, start here.
Staged Mode
Section titled “Staged Mode”Use staged mode when you need to do work between two transactions — typically external API calls that shouldn’t hold a database transaction open:
'charge-payment': { attemptHandler: async ({ job, prepare, complete }) => { // Phase 1: Read state (transaction) const order = await prepare({ mode: "staged" }, async ({ txSql }) => { const [row] = await txSql`SELECT * FROM orders WHERE id = ${job.input.id}`; return row; }); // Transaction closed, heartbeat active
// Phase 2: External API call (no transaction) const { paymentId } = await paymentAPI.charge(order.amount);
// Phase 3: Write results (new transaction) return complete(async ({ finish, txSql }) => { await txSql`UPDATE orders SET payment_id = ${paymentId} WHERE id = ${order.id}`; return finish({ output: { paymentId } }); }); },}Intermediate Transactions with step
Section titled “Intermediate Transactions with step”Within staged mode, step lets you perform multiple independent transactions between prepare and complete — typically for batched work or checkpointed aggregation:
'aggregate-metrics': { attemptHandler: async ({ job, step, complete }) => { let totalProcessed = 0; let cursor;
do { const batch = await step(async ({ txSql }) => { const rows = await txSql` SELECT id, value FROM raw_events WHERE processed = false ORDER BY id LIMIT 500 `; if (rows.length > 0) { await txSql` UPDATE raw_events SET processed = true WHERE id = ANY(${rows.map(r => r.id)}) `; } return { count: rows.length, nextCursor: rows.at(-1)?.id }; }); totalProcessed += batch.count; cursor = batch.count === 500 ? batch.nextCursor : undefined; } while (cursor);
return complete(async ({ finish }) => finish({ output: { totalProcessed } })); },}Each step call opens a fresh guarded transaction (attempt verified), runs the callback, commits, and flushes hooks. If prepare hasn’t been called, step automatically enters staged mode.
When to Use What
Section titled “When to Use What”Do you need to call an external API or do long-runningwork between reading and writing? ├── No → Just call complete() directly (auto-setup atomic) └── Yes → Use prepare({ mode: "staged" }) ├── Single external call → Read in prepare, do external work, write in complete └── Batched transactional work → Use step() between prepare and completeIn practice, explicit prepare with a fixed mode is rarely needed. prepare({ mode: "atomic" }) does the same thing as calling complete directly but with extra ceremony. The main reason to use explicit prepare is when the mode is dynamic — determined at runtime based on job input or application state.
Auto-Setup
Section titled “Auto-Setup”When you skip prepare, Queuert infers the mode from how you call complete:
| Pattern | Mode | What happens |
|---|---|---|
return complete(...) (synchronous) |
Atomic | Single transaction wraps everything |
await something; return complete(...) |
Staged | Heartbeat runs between async work and complete |
This means even without prepare, you can get staged behavior by doing async work before calling complete:
'send-notification': { attemptHandler: async ({ job, complete }) => { await emailService.send(job.input.to, job.input.body);
return complete(async ({ finish, txSql }) => { await txSql`UPDATE notifications SET sent = true WHERE id = ${job.input.id}`; return finish({ output: { sentAt: new Date().toISOString() } }); }); },}Anti-Patterns
Section titled “Anti-Patterns”The exception is dynamic handlers where the mode is determined at runtime — explicit prepare is the right choice there since auto-setup can’t express conditional logic.
See Also
Section titled “See Also”See examples/showcase-processing-modes for a complete working example. See also Job Processing Reliability, Error Handling, Timeouts, and Job Processing reference.