Build and operate queued work
Follow the complete imported Queue example set: define durable data, register and process a job, compose chains and batches, configure retry policy, defer backpressure, and replay terminal failures.
On this page
Source-backed MarkdownIn this guide
- 1Define
Create a validated, JSON-safe QueueableJob.
- 2Compose
Dispatch one job, a chain, or an independent batch.
- 3Recover
Apply backoff, defer backpressure, and replay failures.
Run the installed SQL example
Complete Installation’s tarball/development setup and dedicated SQL test account first. Copy the shipped Queue examples below. These are application helper functions, not self-starting commands; the runner constructs the App, installs a disposable schema, registers the job and cleans up.
The job logs a report identity. For real file-backed report generation, start with the Queue guide. The deterministic output panels farther down illustrate older contract tests; the SQL runner gives the actual independent-consumer result.
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/queue/examples/. examples/Compare chain and batch behavior
Run from your independent app root. Expect empty-chain rejection with zero writes, one initial chain job, one remaining successor after its predecessor, two immediate batch jobs, four successful outcomes and zero jobs left.
npx tsx examples/runQueueWorkflows.tsInspect the complete application runner
The bounded calls deliberately process this lab’s known jobs. A production queue needs registered workers and an application idempotency policy; this demonstration does not guarantee atomic chain handoff after a crash.
import { pathToFileURL } from 'node:url';
import { createGeneratedTestDatabase } from '@db3.ai/app/db/test/db';
import { FailedJob, QueuedJob } from '@db3.ai/app/queue';
import { App } from '@db3.ai/app/server';
import { registerGenerateReportJob } from './createAndProcessReportJob';
import { dispatchReportBatch, dispatchReportPipeline } from './dispatchReportWorkflows';
/**
* Compares sequential chains with immediately queued batches using real SQL.
*
* The example job only logs a report identity. This lab proves orchestration,
* not a reporting provider. It creates and destroys its own test database.
*
* @returns Stable queue counts, validation and processing observations.
*/
export async function runQueueWorkflows() {
const database = await createGeneratedTestDatabase('queue_workflows');
const application = new App({ db: database.db, queue: { queue: 'reports', queueMonitor: false } });
try {
application.log.level = 'silent';
await application.db.install(QueuedJob, FailedJob);
registerGenerateReportJob();
let invalidRejected = false;
try { await dispatchReportPipeline([]); } catch (error) { invalidRejected = error instanceof Error && error.message.includes('at least one report'); }
const afterInvalid = await QueuedJob.query().count();
await dispatchReportPipeline(['first', 'second']);
const chainInitial = await QueuedJob.query().count();
const first = await application.queue.workNextJob('reports');
const chainRemaining = await QueuedJob.query().count();
const second = await application.queue.workNextJob('reports');
const batch = await dispatchReportBatch(['third', 'fourth']);
const batchInitial = await QueuedJob.query().count();
const third = await application.queue.workNextJob('reports');
const fourth = await application.queue.workNextJob('reports');
return { invalidRejected, afterInvalid, chainInitial, chainRemaining, batchInitial, batchIds: batch.length, statuses: [first?.status, second?.status, third?.status, fourth?.status], remaining: await QueuedJob.query().count() };
} finally {
try { await application.close(); } finally { await database.destroy(); }
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log(JSON.stringify(await runQueueWorkflows(), null, 2));
}
Testing
Create a tests/queue directory and save the test below as runQueueWorkflows.test.ts. It imports the example you copied into examples/. Keep the same folder layout so the relative import resolves.
Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout. This database lab needs the same test-only SQL credentials when run through Vitest.
import { expect, it } from 'vitest';
import { runQueueWorkflows } from '../../examples/runQueueWorkflows';
it('rejects an empty chain, then processes sequential and independent jobs on real SQL', async () => {
expect(await runQueueWorkflows()).toEqual({
invalidRejected: true, afterInvalid: 0, chainInitial: 1, chainRemaining: 1,
batchInitial: 2, batchIds: 2, statuses: ['succeeded', 'succeeded', 'succeeded', 'succeeded'], remaining: 0,
});
});
Run and extend the consumer test
Save the exact test in the documented folder, then change the report identities or add one batch job. Keep the invalid-input and final-queue assertions.
npx vitest run tests/queue/runQueueWorkflows.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.tsCoverage and related recovery
This consumer scenario proves real SQL chaining, batch dispatch, queue counts and empty-input recovery. Advanced lease loss, retry state, failure retention and provider backpressure have their own tests and guides. It does not prove Redis conformance or deployment crash safety.
1. Define the job
The constructor validates the same durable data supplied by application code and restored by workers. handle() resolves ordinary services from the active application.
import { app } from '@db3.ai/app/server';
import { QueueableJob } from '@db3.ai/app/queue';
/**
* Durable data required to generate one application report.
*/
export interface GenerateReportJobData extends Record<string, unknown> {
/** Stable application-owned report identifier. */
reportId: string;
}
/**
* Example application job restored from its JSON-safe queue payload.
*/
export class GenerateReportJob extends QueueableJob<GenerateReportJobData> {
/**
* Creates a report job after validating its durable payload.
*
* @param data - Report identity persisted with the queued job.
*/
constructor(data: GenerateReportJobData) {
if (typeof data.reportId !== 'string' || data.reportId.trim() === '') {
throw new Error('GenerateReportJob requires a report id.');
}
super(data);
}
/**
* Generates the report through services resolved from the active application.
*/
async handle(): Promise<void> {
app().log.info({
reportId: this.data.reportId,
}, 'Generating report');
}
}
How it works
- 1
The constructor validates the same durable data used after rehydration.
- 2
Only JSON-safe application identity is persisted in the queue payload.
- 3
handle()resolves normal services from the active application context.
2. Register, dispatch, and process
The worker process registers the class before it claims persisted work. The dispatching process constructs the job, and workNextJob() exposes the detailed attempt outcome.
import { app } from '@db3.ai/app/server';
import type { QueueJobId, QueueProcessResult } from '@db3.ai/app/queue';
import { GenerateReportJob } from './GenerateReportJob';
/**
* Registers the example job in a worker process.
*/
export function registerGenerateReportJob(): void {
app().queue.registerJob(GenerateReportJob);
}
/**
* Dispatches one report job through the active application queue.
*
* @param reportId - Stable application-owned report identifier.
* @returns Driver-owned queued job identifier.
*/
export async function dispatchGenerateReportJob(reportId: string): Promise<QueueJobId> {
return app().queue.dispatch(new GenerateReportJob({ reportId }));
}
/**
* Claims and processes the next job from the default queue.
*
* @returns Queue result, or null when no job is available.
*/
export async function processNextReportJob(): Promise<QueueProcessResult | null> {
return app().queue.workNextJob();
}
{
"jobId": 1,
"status": "succeeded",
"completedJobs": 1,
"remainingJobs": 0
}
How it works
- 1
The worker registers the job class before it claims stored work.
- 2
Dispatch persists the job data and returns the driver-owned id.
- 3
workNextJob()restores a fresh instance and returns the attempt status.
3. Chain dependent work or batch independent work
The pipeline persists the next report only after success. The batch queues every report immediately on the reports worker pool.
import { app } from '@db3.ai/app/server';
import type { QueueJobId } from '@db3.ai/app/queue';
import { GenerateReportJob } from './GenerateReportJob';
/**
* Dispatches report jobs as an ordered pipeline.
*
* Each job is persisted only after its predecessor succeeds. A terminal failure
* therefore prevents the remaining reports from being dispatched.
*
* @param reportIds - Report identifiers to process in order.
* @returns Driver-owned identifier for the first queued job.
*/
export async function dispatchReportPipeline(reportIds: string[]): Promise<QueueJobId> {
if (reportIds.length === 0) {
throw new Error('A report pipeline requires at least one report.');
}
return app().queue.chain(
reportIds.map(reportId => new GenerateReportJob({ reportId })),
{
queue: 'reports',
},
);
}
/**
* Dispatches report jobs independently so workers can process them in parallel.
*
* @param reportIds - Report identifiers that do not depend on one another.
* @returns Driver-owned identifiers for every queued job.
*/
export async function dispatchReportBatch(reportIds: string[]): Promise<QueueJobId[]> {
return app().queue.batch(
reportIds.map(reportId => new GenerateReportJob({ reportId })),
{
queue: 'reports',
},
);
}
{
"pipeline": {
"initiallyQueued": 1,
"completedJobs": 2
},
"batch": {
"jobIds": [
3,
4
],
"queuedJobs": 2,
"queue": "reports"
}
}
How it works
- 1
The chain initially persists one job and dispatches its successor only after success.
- 2
The batch persists both independent jobs immediately on the reports queue.
- 3
Visible aggregate progress still belongs on an application model or Flow.
4. Configure and recover retries
The retry example persists exponential backoff, distinguishes explicit provider backpressure, and replays a failed record without deleting its audit history.
import { app } from '@db3.ai/app/server';
import { QueueRetryLaterError, type QueueJobId } from '@db3.ai/app/queue';
import { GenerateReportJob } from './GenerateReportJob';
/**
* Dispatches one report with an explicit ordinary-failure retry policy.
*
* @param reportId - Stable application-owned report identifier.
* @returns Driver-owned queued job identifier.
*/
export async function dispatchReportWithRetryPolicy(reportId: string): Promise<QueueJobId> {
return app().queue.dispatch(new GenerateReportJob({ reportId }), {
queue: 'reports',
maxTries: 8,
backoff: {
strategy: 'exponential',
initialSeconds: 15,
maxSeconds: 900,
jitter: true,
},
retryUntilSeconds: 21_600,
});
}
/**
* Defers the current job when a provider supplies an explicit retry delay.
*
* Throw this from a QueueableJob handler after detecting provider backpressure.
* Deferral restores the claimed attempt instead of consuming an ordinary retry.
*
* @param delaySeconds - Provider-requested delay before another attempt.
*/
export function retryReportAfterBackpressure(delaySeconds: number): never {
throw new QueueRetryLaterError(delaySeconds, 'The report provider requested backpressure.');
}
/**
* Replays one terminal report failure while preserving its audit record.
*
* @param failedJobId - Driver-owned identifier from failedJobs().
* @returns Driver-owned identifier for the replacement queue job.
*/
export async function retryFailedReportJob(failedJobId: QueueJobId): Promise<QueueJobId> {
return app().queue.retryFailed(failedJobId, {
queue: 'reports',
});
}
{
"policy": {
"queue": "reports",
"maxTries": 8,
"backoff": {
"strategy": "exponential",
"initialSeconds": 15,
"maxSeconds": 900,
"jitter": true
}
},
"backpressure": {
"error": "QueueRetryLaterError",
"delaySeconds": 45
},
"replay": {
"replacementId": 2,
"failedAuditRecords": 1,
"retryOf": {
"failedJobId": "failed_report_01",
"jobUuid": "failed-report-uuid"
}
}
}
How it works
- 1
Ordinary failures consume attempts and use the persisted exponential backoff policy.
- 2
QueueRetryLaterError carries provider backpressure without consuming an ordinary attempt.
- 3
Replay creates a replacement job while preserving the original failed audit record.
Rejects an empty chain, then processes sequential and independent jobs on real SQL.
The guide test passes against the real framework components.packages/app/src/queue/tests/examples/runQueueWorkflows.test.tsThis test command requires the framework repository. Use the walkthrough commands in an installed application.
Environment: Disposable SQL; no external report provider.