All versions since v0.14.0
v0.14.0
What’s Changed
Features
- Caller-supplied job IDs (breaking):
startChain,startChains, and the workercontinueWithcallback now accept an optionalidthat becomes the new chain root (or continuation) job ID. State adapters accept a newvalidateIdpredicate that runs on both adapter-generated and caller-supplied IDs; failures throw the newInvalidJobIdError. When deduplication fires, the existing row’s ID wins over a caller-suppliedid(the returned chain carriesdeduplicated: true). - Aligned adapter ID-generation options (breaking): both PostgreSQL and SQLite adapters now expose a
generateIdfunction. PostgreSQL’sidDefault(SQL expression) is removed in favor of JS-side generation; SQLite’sidGeneratoris renamed togenerateId. A new PostgreSQL migration dropsDEFAULT gen_random_uuid()from theidcolumn. Existing UUID generation behavior is unchanged for default configurations.
Bug Fixes
scheduled_atis now an honest eligibility floor: state adapters clampscheduled_attoMAX(requested, now())oncreateJobs,rescheduleJob, andunblockJobs. Previously, jobs with a pastscheduled_at(or blocked-since-creation jobs whose originalscheduled_atwent stale while they waited) would jump to the front of the acquisition queue ahead of jobs that genuinely became ready earlier.unblockJobsnow preserves a futurescheduled_atset at job creation (it previously reset tonow()unconditionally);triggerJobscontinues to re-anchor tonow()as an explicit reset. Applies to PostgreSQL, SQLite, and in-process adapters; no schema migration or backfill required.lastAttemptErrorcleared on success: when a job completes successfully after a previously failed attempt,completeJobnow resetslast_attempt_errortoNULLalongside the existing status/output/lease updates. Completed rows no longer carry over an error string from a prior failed attempt.requiredAttemptMiddlewaretype fix: a spurious type error fromcreateInProcessWorker’srequiredAttemptMiddlewarecheck is fixed when middleware is typed against a user-suppliedStateAdapteralias (e.g.AttemptMiddleware<MyStateAdapter>). Valid processor slices were being flagged as missing required middleware. Runtime behavior was unaffected; this is a type-only fix.BaseTxContexttightened: changed from{}toRecord<string, unknown>. The previous{}constraint accepted any non-nullish value (string, number, function), letting customStateAdapterauthors pick a non-objectTTxContextwithout a type error. All built-in adapters are already object-shaped, so no runtime behavior changes.
Migration Notes
- PostgreSQL: replace
idDefault: "gen_random_uuid()"withgenerateId: () => crypto.randomUUID()(this is the default and can be omitted). - PostgreSQL: replace
idDefault: "'job.' || gen_random_uuid()::text"withgenerateId: () => `job.${crypto.randomUUID()}`. - SQLite: rename
idGeneratoroption togenerateId. - Custom state adapter implementations must accept an optional
idper entry increateJobsand apply their ownvalidateIdpredicate.
Packages
All packages released at 0.14.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.
v0.14.1
What’s Changed
Improvements
- Adapter conformance suite:
StateConformanceFixturenow propagatesgenerateIdandgenerateInvalidIdthrough to the cases. Previously the runner only forwardedstateAdapterandpoisonTransaction, so adapters configured with a customvalidateIdcould not exercise the caller-suppliedidpath in conformance tests.
Breaking Changes (testing helpers only)
- Removed
StateAdapterConformanceContextandNotifyAdapterConformanceContexttypes. Callers writing conformance suites for external adapters (e.g.it.extend<NotifyAdapterConformanceContext>(...)in vitest specs) should switch toStateConformanceFixture/NotifyConformanceFixture. No runtime API changes — this only affects adapter authors using the conformance harness.
Packages
All packages released at 0.14.1: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.
v0.15.0
What’s Changed
Breaking Changes
triggerJob/triggerJobsrenamed torescheduleJob/rescheduleJobswith an optionalscheduleparameter ({ at: Date } | { afterMs: number }). Omitting it reschedules to now (previous behavior). A past time clamps to now. Onlypendingjobs are reschedulable. Batch validation is atomic.- Error classes renamed:
JobNotTriggerableError/JobsNotTriggerableError→JobNotReschedulableError/JobsNotReschedulableError. New batch error variantsJobsNotFoundErrorandJobsNotReschedulableErrorforrescheduleJobs. Thestatusproperty removed from the singular error; the batch variant exposesjobIds. - Observability event renamed:
jobTriggered→jobRescheduled. Now also emitted on worker retry (not just client reschedules), making it the single signal for “scheduledAt changed.”jobAttemptFailedis now a pure failure event —rescheduledAt/rescheduledAfterMsremoved.scheduleAfterMsdropped fromjobCreatedandjobRescheduledevents. StateProvidergains requiredtransactionConcurrencyfield ("concurrent" | "serialized"). Custom provider authors must declare it.- Dashboard:
POST /jobs/:id/triggerrenamed to/reschedule; “Trigger now” button renamed to “Reschedule”. - OTEL: Counter
queuert.job.triggered→queuert.job.rescheduled; log entryjob_triggered→job_rescheduled.
Features
- Blocker chain cap: A job can declare at most 100 blocker chains.
startChain,startChains, andcontinueWiththrow the newBlockerLimitExceededErrorwhen exceeded. - Dashboard UX improvements: Infinite scroll on list views (chains, jobs, chain detail), real links on cards (right-click/Cmd-click to open in new tab), and smart back navigation that preserves filters.
Bug Fixes
createProcessorstype resolution: Previously resolved job type names from the local slice only; now resolves from all registered slices."incomplete"dedup scope: Multi-step chains that continued past the root job are now correctly matched. Previously, once the root completed, the chain was no longer matched even though it was still running.- Concurrent unblock race (
@queuert/postgres,@queuert/sqlite): Row-level locks on blocked jobs duringunblockJobsprevent a race where concurrent blocker completions could leave dependent jobs stranded inblockedstatus.job_blockerPK widened to support duplicate chain IDs.
Performance
- Autovacuum tuning (
@queuert/postgres): New migration pinsautovacuum_vacuum_threshold = 5000/scale_factor = 0onjobandjob_blockertables, replacing size-proportional0.02. Gives predictable vacuum cadence regardless of table size.
Packages
All packages released at 0.15.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.
v0.15.1
What’s Changed
Bug Fixes
- Saturated workers no longer busy-loop. A worker whose slots were all busy kept polling
getNextJobAvailableInMsas fast as the event loop allowed: with a due job in the backlog the poll reported0ms, so the wait returned immediately and the worker re-entered the loop without being able to take the work. Saturated in-process workers now wait for a slot to free (or forpollIntervalMs) instead of polling.
Performance
- Postgres job acquisition now uses the acquisition index.
acquireJobandgetNextJobAvailableInMsmatched jobs withtype_name IN (...)ordered byscheduled_at, which Postgres cannot satisfy from the(type_name, scheduled_at)index for more than one job type — it fell back to scanning and sorting the entire pending backlog on every acquisition and every poll. Both queries are now per-job-typeLATERALlookups, turning a backlog scan into one index lookup per job type. Workers polling a large Postgres backlog should see a substantial drop in database load. - On Postgres, when several job types are polled together, acquisition now picks a job type at random and takes its oldest due job rather than always taking the globally oldest job. This keeps a backlogged job type from starving the others.
- Postgres
getNextJobAvailableInMsno longer takes row locks on jobs scheduled in the future.
Packages
All packages released at 0.15.1: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.
v0.16.0 Latest
What’s Changed
Queuert 0.16 rebuilds the job model around the chain’s head row and reworks attempt handling around a single finish outcome API, with a one-step schema migration from v0.15.1. It also brings faster listing queries, a redesigned dashboard, and a leaner, more consistent public API. This release contains many breaking changes — review the notes below before upgrading.
Breaking Changes
- New job model — a chain’s head row is now the chain itself.
ChainStatusis justrunning|completed; completed jobs carrycontinuedToId; running jobs exposeattemptAt/attemptBy/attemptUntil. “Lease” is renamed to “attempt” throughout (leaseConfig→attemptConfigwithtimeoutMs/heartbeatMs,LeaseConfig→AttemptConfig, plus matching log events and metrics). - Attempt finalization — handlers end an attempt with
complete(async ({ finish }) => finish({ output | continueWith | reschedule })).rescheduleJob()andRescheduleJobErrorare removed; rescheduling is now a non-error outcome.completeChain’scompleteoption is renamed tohandler. - Renames —
startChain/startChains→createChain/createChains. - Listing API reshaped — flat options with a single
status, requiredtypeName, status-dependentorderBychecked at compile time,root→independent; thechainId/jobId/chainTypeNamefilters are removed;CreatedAtCursor→TimestampWithIdCursor. - Deduplication —
scopeis now required and"incomplete"is renamed to"running"(missing or unknown scopes throwTypeError).windowMsandexcludeChainIdsare removed. - Caller-supplied
idcollisions now throw instead of being silently ignored. cascaderemoved fromdeleteChain/deleteChains.TransactionContextRequiredErrorremoved — a plainErroris thrown instead.wrapHandlernow wraps the whole attempt, so acatcharoundnext()observes handler failures.- Public type exports pruned — internal helper types are no longer exported;
CompletedJobandAttemptConfigare newly exported.
Database Migrations (@queuert/postgres, @queuert/sqlite)
- All migrations are collapsed into
001_initial_schema. You must be on v0.15.1 before upgrading. - PostgreSQL: the upgrade copies tables while v0.15.1 workers keep running; a
migration_locktable makes concurrentmigrateToLatest()calls safe. - SQLite: stop all workers and clients before running the migration.
vacuum()is removed, along with SQLite’scheckForeignKeys/checkAutoVacuumoptions.
Features
steptransaction primitive inside attempt handlers, with a matchingwrapStepmiddleware hook.- Batched lookups —
getChains/getJobs, withChainTypeMismatchError. - Type names & counts —
listChainTypeNames/listJobTypeNamesandcountByChainTypeNames/countByJobTypeNames. - Worker shutdown signal — in-flight handlers receive
"worker_stopping"as the abort reason when the worker stops.
Dashboard
- Full UI redesign: Overview page, Find by ID (⌘K), status tabs, and themes.
- New endpoints for type names, counts, and by-ID lookups. Malformed IDs and unknown
/apipaths return 404; uncaught errors return 500 with an{ error }body.
Observability (@queuert/otel)
- Attempt spans record an
abortevent;prepare/step/completespans record exceptions andERRORstatus. - Breaking: blocker-completion span renamed
resolve chain.*→complete chain.*;queuert.attempt.resultattribute removed (read the outcome from span status); metric names follow the lease → attempt rename. @opentelemetry/apipeer dependency now also accepts^1.0.0.
Performance & Fixes
- Faster listing queries via per-type anchoring.
- PostgreSQL and SQLite skip the redundant
RELEASE SAVEPOINTround-trip. - Type checking is roughly 20–40% cheaper.
AttemptMiddlewaretyped against a concrete adapter now works.chain.inputnow correctly reflects the entry job’s input.- Fixed a race where a job created with blockers could start
pendinginstead ofblocked(#4).
Packages
All packages released at 0.16.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.