# Move a report out of the request

> Accept the work quickly, persist its identity and let a registered worker produce the result. Keep access and progress in the application.

- Package: `@db3.ai/app/queue`
- Canonical page: [https://db3.ai/docs/guide-background](https://db3.ai/docs/guide-background)
- Markdown: [https://db3.ai/docs/guide-background.md](https://db3.ai/docs/guide-background.md)
- Framework source of truth: `packages/app/src/queue/README.md`

<a id="setup"></a>

## Set up the queue and storage

Use Installation’s independent app, tools and dedicated test SQL credentials. The lab creates `QueuedJob` and `FailedJob` tables plus a temporary storage root, then removes them. Production apps apply committed migrations and use storage shared by producer/worker processes.

- [Installation and SQL test account](https://db3.ai/docs/installation.md#database-labs)
- [Full Queue setup](https://db3.ai/docs/queue-overview.md)

<a id="copy"></a>

## Copy the complete example

This recipe reuses the maintained SQL report lab rather than a second version of the same job. All Queue examples are shipped with the package.

### Copy the shipped example

```bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/queue/examples/. examples/
```

<a id="run"></a>

## Run it

Expect the wrong queue to be idle, the first missing-source attempt released, the last attempt failed, a linked replay and one report file containing `Report: Three notes ready`. The final queue is empty and the failed audit record remains.

### Run the lab

```bash
npx tsx examples/runQueueReports.ts
```

<a id="job"></a>

## Give the work a stable identity

The job constructor validates its durable data when dispatching and when restoring. Use a report revision identity, not a request object, database connection, access token or submitted filesystem path.

The handler reads source bytes and replaces a stable result. Running it twice produces one output. That is a property of this operation, not a claim that queues execute exactly once. Email, billing and remote API calls need their own idempotency keys and recovery design.

### examples/WriteReportJob.ts

```typescript
import { QueueableJob } from '@db3.ai/app/queue';
import { app } from '@db3.ai/app/server';

/** Immutable report revision selected and authorized by the application. */
export interface WriteReportData extends Record<string, unknown> {
	/** Safe storage identifier, including a revision when the source can change. */
	reportId: string;
}

/** Rebuilds a local report without appending duplicate output on another attempt. */
export class WriteReportJob extends QueueableJob<WriteReportData> {
	static readonly jobName = 'reports.write.v1';

	/**
	 * Validates both newly dispatched and restored payloads.
	 *
	 * @param data - Trusted report identity; never a caller-supplied storage path.
	 */
	constructor(data: WriteReportData) {
		if (!data || typeof data.reportId !== 'string' || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(data.reportId)) {
			throw new Error('reportId must contain 1–64 lowercase letters, digits or hyphens, starting with a letter or digit.');
		}
		super(data);
	}

	/**
	 * Reads the immutable source and replaces the report at its stable destination.
	 *
	 * Missing source bytes throw normally, allowing Queue to apply its retry policy.
	 * Replacement is repeatable, not an atomic file/database transaction or a
	 * substitute for provider idempotency when sending mail or charging money.
	 */
	async handle(): Promise<void> {
		const source = await app().storage.readToString(`sources/${this.data.reportId}.txt`);
		await app().storage.write(`reports/${this.data.reportId}.txt`, `Report: ${source.trim()}\n`);
	}
}
```

<a id="request"></a>

## Keep the request boundary small

A real request first authenticates, authorizes the report and validates its inputs. Save application-owned report status/ownership, then dispatch the job to the same named queue the worker consumes. Return an accepted response with the application report ID; a queue ID is operational metadata, not read permission.

If saving the report and dispatching must be atomic, use an application outbox or another explicit transactional handoff. A normal model transaction does not include file storage or every queue driver. This framework does not supply a general outbox API today.

The lab exercises producer-to-worker durability without an HTTP endpoint. The API guide supplies the request-validation and owner-scope pattern; an HTTP report-progress UI remains a follow-up.

- [Authenticated JSON API](https://db3.ai/docs/guide-api.md)
- [Transactions and external effects](https://db3.ai/docs/cookbook-transactions.md)

<a id="worker"></a>

## Boot the worker with the same registrations

Register `WriteReportJob` in every worker boot and consume `reports`, not `default`. The producer is closed before a new worker App restores the saved job, so in-memory registration in the request is not mistaken for durability.

For a long-running worker, use `startWorker()` with process supervision and stop/drain before shutdown. A one-step call returning no job means none is ready on that queue now; it does not mean delayed work is complete.

### examples/runQueueReports.ts

```typescript
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
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 { WriteReportJob } from './WriteReportJob';

/**
 * Runs durable dispatch, failure repair, replay and duplicate-safe output on SQL.
 *
 * The producer is closed before a fresh worker App restores persisted work.
 * Only this disposable lab uses Database.install(); deployed apps use migrations.
 * A zero retry delay keeps the failure demonstration deterministic without sleeps.
 *
 * @returns Stable observations also asserted by the shipped documentation test.
 */
export async function runQueueReports() {
	const root = await mkdtemp(join(tmpdir(), 'db3-queue-guide-'));
	try {
		const database = await createGeneratedTestDatabase('queue_guide');
		try {
			const producer = new App({ db: database.db, queue: { driver: 'database', queue: 'reports', queueMonitor: false } });
			try {
				await producer.db.install(QueuedJob, FailedJob);
				await producer.queue.dispatch(new WriteReportJob({ reportId: 'weekly-v1' }), {
					maxTries: 2, backoff: { strategy: 'linear', initialSeconds: 0, maxSeconds: 0, jitter: false },
				});
			} finally {
				await producer.close();
			}

			const consumer = new App({
				db: database.db,
				queue: { driver: 'database', queue: 'reports', queueMonitor: false },
				storage: { disks: { local: { driver: 'local', root } } },
			});
			try {
				consumer.queue.registerJob(WriteReportJob);
				const wrongQueueIdle = await consumer.queue.workNextJob('default') === null;
				const firstAttempt = await consumer.queue.workNextJob('reports');
				const finalAttempt = await consumer.queue.workNextJob('reports');
				const [failed] = await consumer.queue.failedJobs(10);
				if (!failed) throw new Error('Expected the missing source to produce a persisted terminal failure.');

				// Repair the cause before replaying. No queued payload or audit row is edited.
				await consumer.storage.write('sources/weekly-v1.txt', 'Three notes ready');
				const replacementId = await consumer.queue.retryFailed(failed.id);
				const replacement = await QueuedJob.findOrFail(replacementId);
				const replayLinked = replacement.payload?.retryOf?.failedJobId === failed.id;
				const worker = consumer.queue.startWorker('reports', { force: true, maxJobsPerTick: 1 });
				if (!worker) throw new Error('The report worker did not start.');
				await worker.stopAndDrain();

				await consumer.queue.dispatch(new WriteReportJob({ reportId: 'weekly-v1' }));
				const repeated = await consumer.queue.workNextJob('reports');
				const files = [];
				for await (const file of consumer.storage.list('reports')) files.push(file);
				return {
					wrongQueueIdle, firstAttempt: firstAttempt?.status, finalAttempt: finalAttempt?.status,
					replayLinked, repeated: repeated?.status,
					text: await consumer.storage.readToString('reports/weekly-v1.txt'),
					reportFiles: files.length, remainingJobs: await QueuedJob.query().count(),
					retainedFailures: (await consumer.queue.failedJobs(10)).length,
				};
			} finally {
				await consumer.close();
			}
		} finally {
			await database.destroy();
		}
	} finally {
		await rm(root, { recursive: true, force: true });
	}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
	console.log(JSON.stringify(await runQueueReports(), null, 2));
}
```

<a id="recover"></a>

## Repair the cause, then replay

The first run has missing source bytes. Queue records ordinary failure and retries according to its policy. The lab writes the missing source, calls `retryFailed()` and checks linkage to the retained failure record.

Keep result reads scoped to the application owner. Queue history, raw payloads and errors are privileged operations; do not expose them as an unauthenticated progress endpoint.

- [Provider backpressure and deadlines](https://db3.ai/docs/cookbook-retries.md)

<a id="testing"></a>

## Testing

Create a `tests/queue` directory and save the test below as `runQueueReports.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.

### tests/queue/runQueueReports.test.ts

```typescript
import { expect, it } from 'vitest';
import { runQueueReports } from '../../examples/runQueueReports';
import { WriteReportJob } from '../../examples/WriteReportJob';
import expected from '../../examples/outputs/queue-reports.json';

it('restores SQL work in a new App, repairs a failure, drains a worker and avoids duplicate output', async () => {
	expect(await runQueueReports()).toEqual(expected);
});

it('rejects unsafe report identities when constructing or restoring a job', () => {
	for (const reportId of ['', '../secret', '/absolute', 'A'.repeat(65)]) {
		expect(() => new WriteReportJob({ reportId })).toThrow('reportId');
		expect(() => WriteReportJob.fromJSON({ reportId })).toThrow('reportId');
	}
});
```

<a id="run-tests"></a>

## Run and extend the test

Change the immutable report ID and source contents. Keep wrong-queue, failure, linked replay, one-output and empty-queue assertions. Repeat with your production driver in release verification.

### Run your copied test and check types

```bash
npx vitest run tests/queue/runQueueReports.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

<a id="coverage"></a>

## Coverage and next steps

Tested: real SQL handoff across App instances, registration, queue selection, failure/repair/replay, worker drain and repeatable file output. Explained: HTTP ownership, report status, outbox, shared storage and production worker supervision.

- [Queue API](https://db3.ai/docs/queue-api.md)
- [Recurring reports](https://db3.ai/docs/scheduler.md)
- [Multi-step graphs](https://db3.ai/docs/flows.md)

## Behavioural verification
Persists a report job, closes the producer, restores it in a fresh worker, repairs failure and repeats without duplicate output.
- Behaviour test: `packages/app/src/queue/tests/examples/runQueueReports.test.ts`
- Repository test command (framework checkout only): `npm run test:service --workspace @platform/app -- queue --maxWorkers=1`
- In an installed application, use the walkthrough commands instead of this repository test.
- Expected outcome: The guide test passes against the real framework components.
- Environment: Disposable SQL and local files; no Redis, external provider or HTTP server.

## Related documentation
- [Queue](https://db3.ai/docs/queue-overview.md): Create durable background jobs, run named workers, understand every attempt, compose chains and batches, and recover failures through one complete service guide.
- [Scheduler](https://db3.ai/docs/scheduler.md): Decide when a daily task is due, claim its occurrence and hand expensive work to Queue. Inspect what happened without hiding schedule definitions in a database.
- [Run a durable flow](https://db3.ai/docs/flows.md): Use a flow when the steps, values and replay history matter. Start with a small sequential graph, not an arbitrary execution engine.
- [Build an owned-note JSON API](https://db3.ai/docs/guide-api.md): Keep HTTP validation, field conversion and authorization at their own boundaries. Use the starter’s real note routes as the example.

## Framework-owned source: `packages/app/src/queue/README.md`

This is the exact source document captured by the documentation build. Use it for detailed API and workflow guidance, subject to the public package exports and behavioural evidence identified above.

````markdown
# Queue

The installed-package chain/batch lab is
[runQueueWorkflows.ts](./examples/runQueueWorkflows.ts). Copy the shipped Queue
examples and run `npx tsx examples/runQueueWorkflows.ts` with dedicated SQL test
credentials. It rejects an empty pipeline, processes one chain successor at a
time and two independent batch jobs, then destroys its generated database.
`GenerateReportJob` only logs an identity; use the queued-report guide for a job
that reads and writes real report files. This lab does not prove atomic chain
handoff or exactly-once side effects.

## Provider backpressure lab

The shipped `examples/runBackpressure.ts` uses a real SQL queue and a controlled
provider readiness flag. It defers twice without spending a try, succeeds after
readiness changes and terminates expired work using an application deadline.
The companion `tests/examples/runBackpressure.test.ts` is consumer-copyable.
The zero-second delay is only for deterministic testing; production adapters
must validate provider delays and apply their own deadline/idempotency policy.

Move work out of a request without losing it when that process exits. Queue
persists JSON-safe payloads, restores a fresh job for each attempt and records
success, retry, deferral or terminal failure.

Import application APIs from `@db3.ai/app/queue`. Keep the job and its business
policy in your app. Queue owns dispatch, leases and retry transitions; Storage
owns bytes; your application owns authorization, progress and idempotency.

## Run a real report

Start with Node.js 24 and an independent ESM app from
[Installation](https://db3.ai/docs/installation). Before npm publication, install
matching App and Pure tarballs supplied by a maintainer. Add `tsx`,
`typescript`, `@types/node` and `vitest` as development dependencies.

The report lab needs local MariaDB and a dedicated test account that may create
and drop only `db3_app_test_*` databases. Do not use production credentials.
Save test-only settings in the app-root `.env`, exclude it from Git, and unset
`DATABASE_URL` unless it intentionally points at this test server:

```dotenv
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=your_test_user
DB_PASSWORD=your_test_password
DB_DATABASE=db3_app_test
DB_TEST_DATABASE_PREFIX=db3_app_test
```

Copy the shipped examples into your app and run:

```sh
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/queue/examples/. examples/
npx tsx examples/runQueueReports.ts
```

The lab creates an isolated database and local temporary directory. It removes
both in `finally`; killing the process forcibly can leave temporary resources.
It does not touch the starter's notes database or call an AI/email provider.

Read these actual example sources:

- [WriteReportJob.ts](./examples/WriteReportJob.ts): validate a stable revision
	identifier and replace its output instead of appending duplicate content.
- [runQueueReports.ts](./examples/runQueueReports.ts): close the producer, restore
	SQL-backed work in a fresh App, repair a missing source and replay a failure.
- [queue-reports.json](./examples/outputs/queue-reports.json): asserted output.
- [reportConsole.ts](./examples/reportConsole.ts): application-owned CLI adapter.

Expected result:

```json
{
	"wrongQueueIdle": true,
	"firstAttempt": "released",
	"finalAttempt": "failed",
	"replayLinked": true,
	"repeated": "succeeded",
	"text": "Report: Three notes ready\n",
	"reportFiles": 1,
	"remainingJobs": 0,
	"retainedFailures": 1
}
```

The missing source deliberately fails twice. The lab fixes the cause, replays
the failed record and drains a polling worker. The original failure remains for
audit. Another dispatch replaces the same report, leaving one output.
Zero-second backoff is used only to keep the test deterministic.

## Run it in the starter

The disposable lab uses `Database.install()`. Your app must use migrations.

In the Notes + AI starter, add `QueuedJob` and `FailedJob` from
`@db3.ai/app/queue` to the existing `models` array in
`server/database/models.ts`, retaining every current model:

```sh
npm run db:make:migration -- add_queue_tables
# Review the generated migration before applying it.
npm run db:migrate
npm run db:check
```

Now use your development app's database settings, not the test-only lab
configuration. The copied console adapter loads `.env`, registers
`WriteReportJob` and uses the database driver. Its local storage root is
`data/queue-reports`, or the absolute `REPORT_STORAGE_ROOT` you supply.
Exclude `data/` from Git. Producer and worker must share that storage.

```sh
npx tsx examples/reportConsole.ts report:seed
npx tsx examples/reportConsole.ts queue:dispatch reports.write.v1 '{"reportId":"weekly-v1"}' --queue=reports --tries=2
npx tsx examples/reportConsole.ts queue:work --queue=reports --once
npx tsx examples/reportConsole.ts queue:failed
```

Expect a dispatch ID, a successful processing line, and
`data/queue-reports/reports/weekly-v1.txt` containing
`Report: Three notes ready`. `report:seed` and `queue:failed` are example-owned
commands. They write fixed sample input and list safe, bounded failure metadata.

For a continuous worker, use a separate terminal:

```sh
npx tsx examples/reportConsole.ts queue:work --queue=reports --max-jobs=1
```

Ctrl-C drains the active tick before closing the App. To replay a repaired
terminal failure, use its failed-record ID, not its original active-job ID:

```sh
npx tsx examples/reportConsole.ts queue:retry 123 --queue=reports
```

Replace `123` with a real ID from `queue:failed`. Repeated replay calls each
create another job; replay is not deduplicated. In your own app, reuse the same
bootstrap/configuration for the HTTP process and worker without starting an HTTP
listener in the worker. These are adapter command names, not global executables.

## Design the payload

Extend `QueueableJob`, validate constructor data and implement `handle()`.
The inherited `toJSON()` and `fromJSON()` cover normal JSON data. Set an explicit
stable `jobName`, such as `reports.write.v1`; class names otherwise become
durable names. Keep old names/payload versions supported while stored work can
still reference them.

Store immutable identities or versioned inputs, not open connections, request
objects, API keys or large file bodies. Load current application data in the
handler and recheck permission/state if access can be revoked after dispatch.

Dispatching a constructed job auto-registers its class in that process only.
Every separate worker must call `registerJob()` for all reachable jobs or supply
a `jobResolver`. `registerHandler()` is available for a stable named function
handler; it does not supply class constructor validation automatically.

## Queue names, timing and capacity

A `reports` worker only claims `reports` jobs. `delaySeconds` sets earliest
availability, not a guaranteed execution time. `workNextJob()` processes at most
one available job and returns `null` when idle. Its statuses are `succeeded`,
`released`, `deferred`, `failed` and `lease_lost`.
`processNextJob()` returns whether any job was processed, not whether it succeeded.

`startWorker()` starts a polling loop. `workerEnabled: false` or
`QUEUE_WORKER=false` disables it unless forced. `maxJobsPerTick` bounds sequential
work per tick; it is not concurrency. Use more supervised worker processes for
parallelism. Polling defaults to 1000 ms and valid intervals are clamped to at
least 100 ms. `queue:work --once` checks once and exits, even if delayed work exists.

## Retries and repair

Ordinary errors consume attempts. Dispatch defaults are three attempts,
exponential backoff starting at 15 seconds, capped at 3600 seconds, with jitter
off unless changed in queue options/environment. The supported strategies are
`linear` and `exponential`. For constant delay set `initialSeconds` and
`maxSeconds` equal; there is no `fixed` strategy.

[manageReportRetries.ts](./examples/manageReportRetries.ts) shows bounded backoff,
provider backpressure and replay. `retryUntilSeconds` bounds scheduling another
ordinary retry; it is not an execution deadline and does not abort a running job.
Throw `QueueRetryLaterError` only for deliberate backpressure. It restores the
attempt and bypasses the ordinary retry window, so the app must own a separate
deadline to prevent immortal work.

`failedJobs(limit)` reads recent failures. `retryFailed(id, options)` creates a
new UUID linked by `payload.retryOf` and preserves the old failure. Repair the
cause and check that the operation is still authorized before replaying. The
replacement receives a fresh retry policy/window; the original deadline is not
a continuing replay cutoff.

`onRetry()` runs after release, `onFinalFailure()` after persisted failure.
Deliberate deferrals call neither. Hook/listener errors go to
`onLifecycleError` and cannot reverse the transition. Use durable application
state for important recovery, not a hook that must never fail.

## Leases are not exactly-once execution

Database and Redis drivers renew reservations during execution, approximately
every third of `retryAfterSeconds` (at least one second). They check ownership
again before the outcome transition. Mutations are fenced by job ID and attempt.

A crash, blocked event loop or lost connection can let another worker claim
the job after expiry. `lease_lost` means this worker cannot safely commit the
queue outcome; the handler may already have performed external work.
`retryAfterSeconds` is a lease duration, not a timeout or cancellation API.

Make side effects idempotent. The report example replaces one stable output for
immutable input, but that is not an atomic file/database transaction. Email and
payments need application/provider idempotency. Avoid a non-transactional
"already done" read followed by an unprotected write.

## Chains and batches

[dispatchReportWorkflows.ts](./examples/dispatchReportWorkflows.ts) demonstrates
`chain()` for ordered work and `batch()` for independent jobs. Register every
job that can appear in a chain, not just its first class.

A chain persists the first job with its remaining steps. Success dispatches the
next step before deleting the predecessor. Those transitions are not atomic: a
crash between them can duplicate the successor. Every step still needs
idempotency and important workflows need reconciliation.

Successors inherit queue, maximum attempts and backoff. They do not inherit the
root's origin, initial delay or ordinary retry deadline. A terminal failure
stops advancement. Replaying its stored failure can resume the remaining chain.

A batch dispatches sequentially and returns IDs; it is not an all-or-nothing
transaction or a tracked batch record. Earlier dispatches remain if a later one
fails. There are no built-in batch counters, cancellation or completion callbacks.

## Transactions and dispatch

The default `app().queue` does not join a surrounding ActiveRecord transaction.
Do not enqueue before commit and assume rollback removes the job or workers
cannot see it. Commit, then dispatch for the simple case.

That still leaves a crash window between commit and dispatch. For work that
must not be missed, write an application outbox in the same transaction and
publish it with idempotency and reconciliation. No built-in `afterCommit()` or
transactional outbox contract exists yet.

## Drivers and shutdown

The database driver needs migrated `QueuedJob` and `FailedJob` tables.
Redis stores jobs, ready/delayed/reserved state and failures under a configurable
prefix. Choosing Redis does not migrate queued SQL work.

For Redis, configure a dedicated app/environment prefix and an appropriate
persistence, eviction and authentication/TLS policy. A prefix is not access
control. Retain the concrete driver you inject:

```ts
import { RedisQueueDriver } from '@db3.ai/app/queue';
import { App } from '@db3.ai/app/server';

const driver = new RedisQueueDriver({
	url: process.env.QUEUE_REDIS_URL,
	keyPrefix: 'my-app:development:queue',
});
const application = new App({ queue: { driver, queue: 'reports' } });
```

Stop and drain workers first, then `await driver.close()`, then
`await application.close()`. `App.close()` does not stop Queue workers or close
the Redis Queue driver. Adapt a Redis console host's `close()` callback to close
both resources after worker drain.

`stop()` stops future polling; `stopAndDrain()` waits for the current tick,
which can include several jobs. Neither aborts a handler. Give supervisors enough
shutdown grace time and restart workers after code changes.

## Progress, monitoring and privacy

Use `queue.events.subscribe()` for lifecycle observations; it returns an
unsubscribe function. Listeners are awaited after transitions. Slow listeners
delay the worker; exceptions cannot undo already committed state.

Successful jobs leave active storage. Keep user-visible progress/output
identities on application models. `QueueMonitor` is development telemetry, not
durable history or an accounting ledger. Monitor backlog age, retries, terminal
failures and ambiguous lease losses.

Payloads, lifecycle events and stored exceptions can expose sensitive values.
Never queue secrets; redact before exporting telemetry, restrict access to
inspection/replay and define retention. There is no automatic failure pruning.

## Troubleshooting

- Idle worker: check the exact queue name, availability time and reservation.
- Missing handler: fix registration/deployed job name, then inspect and replay.
- Missing SQL table: apply queue migrations against this process's database.
- Missing file: ensure workers share the same storage and immutable source.
- Hanging shutdown: drain the active tick and close any owned Redis driver.
- `failed` status with exit code zero: finite console commands report outcomes
	in logs; shell success does not guarantee job success.
- Infrastructure failure: `workNextJob()` can reject before returning a result.
	Do not turn an unavailable database into a silent empty queue.

## Test the workflow

The [Queue guide](https://db3.ai/docs/queue-overview#testing) renders the exact
service-owned test. Save it as `tests/queue/runQueueReports.test.ts`, retaining
the example and `outputs/` directory above:

```sh
npx vitest run tests/queue/runQueueReports.test.ts --maxWorkers=1
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --resolveJsonModule --skipLibCheck examples/*.ts
```

It uses real SQL, files and Queue, covering failure/repair/replay, App lifetime,
worker drain, repeated output and invalid identities. Tests fail when SQL is
unavailable rather than skipping this documented path.

For source-repository maintenance:

```sh
npm run test:service --workspace packages/app -- queue --maxWorkers=1
npm run check --workspace packages/app
npm run framework:package:test
```

Older deterministic-driver tests execute the small chain/batch/backpressure
examples. Built-in-driver suites separately exercise SQL/Redis leases and
transitions. Redis tests may skip locally when unavailable; release verification
must set `TEST_REDIS_REQUIRED=1` and supply real infrastructure. A passed SQL
report lab is not proof of Redis operational readiness.

## Reference, coverage and follow-up

The [Queue API reference](https://db3.ai/docs/queue-api) renders freshly emitted
service, job, payload, retry, worker, driver, Redis and console contracts.
Use `@db3.ai/app/queue` imports, not internal declaration paths.

Taught and executed here: SQL persistence, registration, named queues, retries,
terminal history, repair/replay, worker drain, repeatable output and input
validation. Explained with separate tests/reference: leases, Redis, lifecycle
observers, hooks, chains, batches and console adapters.

Not implemented: hard job timeout/cancellation, built-in unique jobs, atomic
chain handoff, first-class tracked batches, retry-all, automatic pruning or a
transactional outbox. Follow-up walkthroughs: shared-storage deployment,
provider-backed AI budgets, idempotent email, and failure-retention operations.

Queue owns its contracts, drivers, examples and tests. It depends on Database,
Logging and App composition; Scheduler and Flows consume its lifecycle.
````

## Guidance for AI tools
Use the documented public import `@db3.ai/app/queue` and its exported types. Prefer the source-backed examples and behavioural outcomes above over invented APIs or source-relative internal imports.
