← All @molecule/* packages · App templates
@molecule/api-cron-bullmqProvider bond · cron · API (Node) · v1.0.1 · Apache-2.0
BullMQ distributed cron scheduling provider for molecule.dev
npm install @molecule/api-cron-bullmqnpm · Source on GitHub · Implements @molecule/api-cron
@molecule/api-cron-bullmq is a provider bond on the API (Node) side: it implements the cron core interface (@molecule/api-cron) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider, schedule } from '@molecule/api-cron'
import { createProvider } from '@molecule/api-cron-bullmq'
const provider = createProvider({
connection: { host: 'localhost', port: 6379 },
onError: (error) => console.error('cron Redis connection error', error),
})
setProvider(provider)
await schedule('cleanup', '0 3 * * *', async () => {
console.log('Nightly cleanup')
})Works with: @molecule/api-bond, @molecule/api-cron
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
BullMQ cron scheduling provider for molecule.dev.
Implements the CronProvider interface using BullMQ repeatable jobs backed
by Redis. The schedule (the repeatable job definition in Redis) is
persistent and distributed — it survives process restarts and produces
ticks that any worker process sharing the queue can pick up. The handler
function itself, and this bond's local list()/pause state, are NOT
persisted — see the remarks below.
import { setProvider, schedule } from '@molecule/api-cron'
import { createProvider } from '@molecule/api-cron-bullmq'
const provider = createProvider({
connection: { host: 'localhost', port: 6379 },
onError: (error) => console.error('cron Redis connection error', error),
})
setProvider(provider)
await schedule('cleanup', '0 3 * * *', async () => {
console.log('Nightly cleanup')
})
provider
npm install @molecule/api-cron-bullmq @molecule/api-bond @molecule/api-cron bullmq
BullMQCronConfigConfiguration options for the BullMQ cron provider.
interface BullMQCronConfig {
/** Redis connection options. */
connection: RedisConnectionOptions
/** Queue name prefix. Defaults to `'molecule-cron'`. */
queueName?: string
/** Default timezone for all jobs. */
timezone?: string
/**
* Called whenever the underlying BullMQ queue or worker emits a
* connection-level `'error'` event (e.g. Redis unreachable). These errors
* are always logged via the bonded logger regardless of this callback —
* use it for additional handling (alerting, metrics, a fail-fast exit).
*/
onError?: (error: Error) => void
}
RedisConnectionOptionsRedis connection options for BullMQ.
interface RedisConnectionOptions {
/** Redis host. Defaults to `'localhost'`. */
host?: string
/** Redis port. Defaults to `6379`. */
port?: number
/** Redis password. */
password?: string
/** Redis database number. */
db?: number
/**
* Full Redis connection URL. Takes precedence over `host`/`port`/`password`/
* `db` (ioredis parses the URL first, so those fields only fill gaps the URL
* leaves). Use the `rediss://` scheme for a TLS endpoint — it enables TLS
* automatically, same as passing `tls`.
*/
url?: string
/**
* TLS options for the Redis connection, forwarded to the ioredis client
* BullMQ builds. Pass `true` (shorthand for `{}`, ioredis' "TLS with
* defaults") to enable TLS, or a `tls.ConnectionOptions` object
* (`ca`/`cert`/`key`/`rejectUnauthorized`/`servername`/…) for a managed
* Redis that requires it. Omit (or `false`) for a plaintext connection. A
* `rediss://` `url` also enables TLS on its own.
*/
tls?: boolean | TLSConnectionOptions
}
createProvider(config)Creates a BullMQ cron provider.
function createProvider(config: BullMQCronConfig): CronProvider
config — Provider configuration including Redis connection.Returns: A CronProvider backed by BullMQ repeatable jobs.
Implements @molecule/api-cron interface.
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-cron ^1.0.1@molecule/api-bond
@molecule/api-cron
bullmq
schedule() must be called for every job on every process boot —
including after a restart. The repeatable job scheduler lives in Redis
and keeps ticking across restarts, but the JavaScript handler function
passed to schedule() only lives in this process's memory. A tick for a
job this process hasn't (re-)registered logs a warning
("...has no registered handler in this process...") and no-ops rather
than silently dropping the tick.
list() and getStatus-style reads only reflect jobs registered on
THIS process — they do NOT query Redis for jobs registered by other
worker processes. cancel() still works for a job unknown to this
process's memory (it falls through to queue.removeJobScheduler).
pause()/resume() are cluster-wide: a paused flag is written to Redis
and checked by every worker sharing the queue on every tick, so pausing
on one process stops execution on all of them (not just the caller).
Note this only affects the handler running — BullMQ still records a
normal 'completed' entry for the skipped tick; that's expected, not a
hidden error.
Worker/job errors are never swallowed: a thrown handler is logged (with
the job name) AND rethrown so BullMQ marks that occurrence 'failed' —
the repeatable schedule itself keeps ticking (a transient failure does
not kill the job, same semantics as the node-cron bond and real
crontab). Queue/worker-level connection errors (e.g. Redis unreachable)
are logged with an actionable message instead of the silent hang you'd
otherwise get while ioredis retries the connection indefinitely; pass
onError for additional handling.
CronOptions.noOverlap: true is emulated per-worker-process (an
in-memory running flag) — it prevents a slow handler from overlapping
itself on the SAME worker, but does not coordinate across multiple
distributed worker processes running the same job concurrently.
Integration checklist — drive the real flow (no mocks), adapt each item to this app's actual scheduled jobs, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
list() returns each one (or its schedule() call ran without error) — a
job that never registers never fires.runNow(jobId) (or invoke the handler directly) and assert the effect;
never stub the body. COUNTERPARTY: the sandbox process is short-lived, so a
real timed tick may never arrive — that is expected. Verify by direct
invocation, not by waiting minutes for the schedule to fire.status reflects it.cron expression and confirm it
matches the intended schedule (nightly, hourly, …) — verify by reading it,
not by waiting for a tick.