Skip to content

Queuert

Durable, typed job chains that commit with your database transactions. A job-chain library that lives in your database — chains compose like Promises, but persist. No Redis required, no separate server.
Diagram

Define a typed job chain. Each step’s input, output, and continuation are inferred — wrong-shape continuations are compile errors.

const jobTypes = defineJobTypes<{
"provision-account": {
entry: true;
input: { userId: number };
continueWith: { typeName: "send-welcome-email" };
};
"send-welcome-email": {
input: { userId: number; accountId: string };
continueWith: { typeName: "sync-to-crm" };
};
"sync-to-crm": {
input: { userId: number; accountId: string };
};
}>();

Start the chain inside your DB transaction. If the transaction rolls back, the chain is never created.

const client = await createClient({ stateAdapter, jobTypes });
await withTransactionHooks(async (transactionHooks) =>
db.transaction(async (tx) => {
const user = await tx.users.create({ name: "Alice", email: "alice@example.com" });
await client.startChain({
tx,
transactionHooks,
typeName: "provision-account",
input: { userId: user.id },
// ↑ wrong shape here is a compile error
});
}),
);

Each handler continues with the next step. The compiler enforces that continueWith matches the declared next type’s input.

const worker = await createInProcessWorker({
client,
processors: createProcessors({
client,
jobTypes,
processors: {
"provision-account": {
attemptHandler: async ({ job, complete }) => {
const accountId = await provisionAccount(job.input.userId);
return complete(async ({ continueWith }) =>
continueWith({
typeName: "send-welcome-email",
input: { userId: job.input.userId, accountId },
// ↑ missing accountId would be a compile error
}),
);
},
},
// ...handlers for "send-welcome-email" and "sync-to-crm"
},
}),
});
const stop = await worker.start();

Transactional, both ends

Enqueue commits inside your DB transaction; handler completion and next-step continueWith commit in the same transaction as your domain writes. For DB-bound work, no outbox and no idempotency-key ritual — both halves are structural.

Typed job chains

Inputs, outputs, continuations, and blockers infer end-to-end via defineJobTypes. Refactoring is compiler-checked.

Lives in your database

Postgres or SQLite. No Redis required, no workflow server, no separate persistence tier to operate.

Sub-second wakeup

LISTEN/NOTIFY (or Redis pub/sub, or NATS) wakes workers when a row commits — not on a polling timer.

Fan-in via blockers

“Wait for these N independent chains to finish, then run X” is a typed primitive — not glue code.

Schedule for later

Delay a chain to a specific time or duration. Schedule retries with backoff. Future work, no extra infrastructure.

Deduplication

Pass a deduplication key on enqueue. Identical keys collapse to a single chain — at-most-once, by construction.

Lean and battle-tested

Zero runtime dependencies in every package — driver libraries are peerDependencies you already own. 4,000+ tests across adapters and a shared conformance suite every state and notify adapter must pass.

MIT licensed

No vendor lock-in. No enterprise tier. Own your stack.

Databases

PostgreSQL · SQLite · in-process · any database via custom adapters

ORMs

Kysely · Drizzle · Prisma · any ORM via custom adapters

Drivers

pg · postgres.js · node:sqlite · better-sqlite3 · any driver via custom adapters

Notifications

Redis · PostgreSQL LISTEN/NOTIFY · NATS · in-process · any broker via custom adapters

Validation

Zod · ArkType · Valibot · TypeBox · any schema library via custom adapters

Observability

OpenTelemetry · Embeddable web UI dashboard