[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-10156":3},{"id":4,"name":5,"fullName":6,"owner":5,"repo":5,"description":7,"homepage":8,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":16,"stars7d":14,"stars30d":17,"stars90d":15,"forks30d":15,"starsTrendScore":18,"compositeScore":19,"rankGlobal":9,"rankLanguage":9,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":23,"hasPages":23,"topics":24,"createdAt":9,"pushedAt":9,"updatedAt":42,"readmeContent":43,"aiSummary":44,"trendingCount":15,"starSnapshotCount":15,"syncStatus":16,"lastSyncTime":45,"discoverSource":46},10156,"agenda","agenda\u002Fagenda","Lightweight job scheduling for Node.js","https:\u002F\u002Fagenda.github.io\u002Fagenda\u002F",null,"HTML",9674,842,117,7,0,2,18,9,39.78,"Other",false,"main",true,[25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41],"automated","cron","cronjob","crontab","frequency","interval","job","job-processor","job-scheduler","mongodb","nodejs","queue","recurring","runner","schedule","scheduler","task","2026-06-12 02:02:17","# Agenda\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002Fagenda\u002Fagenda@main\u002Fagenda.svg\" alt=\"Agenda\" width=\"100\" height=\"100\">\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  A light-weight job scheduling library for Node.js\n\u003C\u002Fp>\n\n> **Migrating from v5?** See the [Migration Guide](docs\u002Fmigration-guide-v6.md) for all breaking changes.\n\n## What's New in v6\n\n- **ESM-only** - Modern ES modules (Node.js 18+)\n- **Pluggable backend system** - New `AgendaBackend` interface for storage and notifications\n- **Real-time notifications** - Optional notification channels for instant job processing\n- **MongoDB 6 driver** - Updated to latest MongoDB driver\n- **Persistent job logging** - Optional structured logging of job lifecycle events to database\n- **Monorepo** - Now includes `agenda`, `agendash`, and `agenda-rest` packages\n\n## Key Features\n\n- Complete rewrite in TypeScript (fully typed!)\n- **Pluggable backend** - MongoDB by default, implement your own (see [Custom Backend Driver](docs\u002Fcustom-database-driver.md))\n- **Real-time notifications** - Use Redis, PostgreSQL LISTEN\u002FNOTIFY, MongoDB Change Streams (Native) or custom pub\u002Fsub\n- MongoDB 6 driver support\n- `touch()` with optional progress parameter (0-100)\n- `getRunningStats()` for monitoring\n- Fork mode for sandboxed job execution\n- Automatic connection handling\n- Creates indexes automatically by default\n\n# Agenda offers\n\n- Minimal overhead. Agenda aims to keep its code base small.\n- Mongo backed persistence layer.\n- Promises based API.\n- Scheduling with configurable priority, concurrency, repeating and persistence of job results.\n- Scheduling via cron or human readable syntax.\n- Event backed job queue that you can hook into.\n- [Agenda-rest](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fagenda\u002Ftree\u002Fmain\u002Fpackages\u002Fagenda-rest): optional standalone REST API.\n- [Inversify-agenda](https:\u002F\u002Fgithub.com\u002Flautarobock\u002Finversify-agenda) - Some utilities for the development of agenda workers with Inversify.\n- [Agendash](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fagenda\u002Ftree\u002Fmain\u002Fpackages\u002Fagendash): optional standalone web-interface.\n\n### Feature Comparison\n\nSince there are a few job queue solutions, here a table comparing them to help you use the one that\nbetter suits your needs.\n\n| Feature                    |     BullMQ      |      Bull       |   Bee    |       pg-boss       |         Agenda          |\n| :------------------------- | :-------------: | :-------------: | :------: | :-----------------: | :---------------------: |\n| Backend                    |      redis      |      redis      |  redis   |      postgres       | mongo, postgres, redis  |\n| Status                     |     Active      |   Maintenance   |  Stale   |       Active        |         Active          |\n| TypeScript                 |        ✓        |                 |          |          ✓          |            ✓            |\n| Priorities                 |        ✓        |        ✓        |          |          ✓          |            ✓            |\n| Concurrency                |        ✓        |        ✓        |    ✓     |          ✓          |            ✓            |\n| Delayed jobs               |        ✓        |        ✓        |          |          ✓          |            ✓            |\n| Global events              |        ✓        |        ✓        |          |                     |            ✓            |\n| Rate Limiter               |        ✓        |        ✓        |          |          ✓          |                         |\n| Debouncing                 |        ✓        |                 |          |          ✓          |            ✓            |\n| Pause\u002FResume               |        ✓        |        ✓        |          |                     |            ✓            |\n| Sandboxed worker           |        ✓        |        ✓        |          |                     |            ✓            |\n| Repeatable jobs            |        ✓        |        ✓        |          |          ✓          |            ✓            |\n| Auto-retry with backoff    |        ✓        |        ✓        |          |          ✓          |            ✓            |\n| Dead letter queues         |        ✓        |        ✓        |          |          ✓          |                         |\n| Job dependencies           |        ✓        |                 |          |                     |                         |\n| Atomic ops                 |        ✓        |        ✓        |    ✓     |          ✓          |            ~            |\n| Persistence                |        ✓        |        ✓        |    ✓     |          ✓          |            ✓            |\n| UI                         |        ✓        |        ✓        |          |                     |            ✓            |\n| REST API                   |                 |                 |          |                     |            ✓            |\n| Central (Scalable) Queue   |        ✓        |                 |          |          ✓          |            ✓            |\n| Supports long running jobs |                 |                 |          |                     |            ✓            |\n| Human-readable intervals   |                 |                 |          |                     |            ✓            |\n| Real-time notifications    |        ✓        |                 |          |          ✓          |            ✓            |\n| Optimized for              | Jobs \u002F Messages | Jobs \u002F Messages | Messages |        Jobs         |          Jobs           |\n\n_Kudos for making the comparison chart goes to [Bull](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fbull#feature-comparison) maintainers._\n\n# Installation\n\nInstall via NPM\n\n    npm install agenda\n\n**For MongoDB:** Install the official MongoDB backend:\n\n    npm install @agendajs\u002Fmongo-backend\n\nYou will need a working [MongoDB](https:\u002F\u002Fwww.mongodb.com\u002F) database (v4+).\n\n**For PostgreSQL:** Install the official PostgreSQL backend:\n\n    npm install @agendajs\u002Fpostgres-backend\n\n**For Redis:** Install the official Redis backend:\n\n    npm install @agendajs\u002Fredis-backend\n\n# Example Usage\n\n```js\nimport { Agenda } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\nconst mongoConnectionString = 'mongodb:\u002F\u002F127.0.0.1\u002Fagenda';\n\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ address: mongoConnectionString })\n});\n\n\u002F\u002F Or override the default collection name:\n\u002F\u002F const agenda = new Agenda({\n\u002F\u002F   backend: new MongoBackend({ address: mongoConnectionString, collection: 'jobCollectionName' })\n\u002F\u002F });\n\n\u002F\u002F or pass in an existing MongoDB Db instance\n\u002F\u002F const agenda = new Agenda({\n\u002F\u002F   backend: new MongoBackend({ mongo: myMongoDb })\n\u002F\u002F });\n\nagenda.define('delete old users', async job => {\n\tawait User.remove({ lastLogIn: { $lt: twoDaysAgo } });\n});\n\n(async function () {\n\t\u002F\u002F IIFE to give access to async\u002Fawait\n\tawait agenda.start();\n\n\tawait agenda.every('3 minutes', 'delete old users');\n\n\t\u002F\u002F Alternatively, you could also do:\n\tawait agenda.every('*\u002F3 * * * *', 'delete old users');\n})();\n```\n\n```js\nagenda.define(\n\t'send email report',\n\tasync job => {\n\t\tconst { to } = job.attrs.data;\n\t\tawait emailClient.send({\n\t\t\tto,\n\t\t\tfrom: 'example@example.com',\n\t\t\tsubject: 'Email Report',\n\t\t\tbody: '...'\n\t\t});\n\t},\n\t{ priority: 'high', concurrency: 10 }\n);\n\n(async function () {\n\tawait agenda.start();\n\tawait agenda.schedule('in 20 minutes', 'send email report', { to: 'admin@example.com' });\n})();\n```\n\n```js\n(async function () {\n\tconst weeklyReport = agenda.create('send email report', { to: 'example@example.com' });\n\tawait agenda.start();\n\tawait weeklyReport.repeatEvery('1 week').save();\n})();\n```\n\n# Full documentation\n\nSee also https:\u002F\u002Fagenda.github.io\u002Fagenda\u002F\n\nAgenda's basic control structure is an instance of an agenda. Agenda's are\nmapped to a database collection and load the jobs from within.\n\n## Table of Contents\n\n- [Migration Guide (v5 to v6)](docs\u002Fmigration-guide-v6.md)\n- [Configuring an agenda](#configuring-an-agenda)\n  - [Real-Time Notifications](#real-time-notifications)\n- [Agenda Events](#agenda-events)\n- [Defining job processors](#defining-job-processors)\n- [Automatic Retry with Backoff](#automatic-retry-with-backoff)\n- [Job Debouncing](#job-debouncing)\n- [Auto-Cleanup of Completed Jobs](#auto-cleanup-of-completed-jobs)\n- [Persistent Job Logging](#persistent-job-logging)\n- [Creating jobs](#creating-jobs)\n- [Managing jobs](#managing-jobs)\n- [Starting the job processor](#starting-the-job-processor)\n- [Multiple job processors](#multiple-job-processors)\n- [Manually working with jobs](#manually-working-with-a-job)\n- [Job Queue Events](#job-queue-events)\n- [Frequently asked questions](#frequently-asked-questions)\n- [Example Project structure](#example-project-structure)\n- [Known Issues](#known-issues)\n- [Debugging Issues](#debugging-issues)\n- [Acknowledgements](#acknowledgements)\n\n## Configuring an agenda\n\nPossible agenda config options:\n\n```ts\n{\n\t\u002F\u002F Required: Backend for storage (and optionally notifications)\n\tbackend: AgendaBackend;\n\t\u002F\u002F Optional: Override notification channel from backend\n\tnotificationChannel?: NotificationChannel;\n\t\u002F\u002F Agenda instance name (used in lastModifiedBy field)\n\tname?: string;\n\t\u002F\u002F Job processing options\n\tdefaultConcurrency?: number;\n\tprocessEvery?: string | number;\n\tmaxConcurrency?: number;\n\tdefaultLockLimit?: number;\n\tlockLimit?: number;\n\tdefaultLockLifetime?: number;\n\t\u002F\u002F Auto-remove one-time jobs after successful completion\n\tremoveOnComplete?: boolean;\n\t\u002F\u002F Persistent job logging\n\tlogging?: boolean | JobLogger | { logger?: JobLogger; default?: boolean };\n\t\u002F\u002F Fork mode options\n\tforkHelper?: { path: string; options?: ForkOptions };\n\tforkedWorker?: boolean;\n}\n```\n\n**MongoBackend config options:**\n\n```ts\n{\n\t\u002F\u002F MongoDB connection string\n\taddress?: string;\n\t\u002F\u002F Or existing MongoDB database instance\n\tmongo?: Db;\n\t\u002F\u002F Collection name (default: 'agendaJobs')\n\tcollection?: string;\n\t\u002F\u002F MongoDB client options\n\toptions?: MongoClientOptions;\n\t\u002F\u002F Create indexes on connect (default: true)\n\tensureIndex?: boolean;\n\t\u002F\u002F Sort order for job queries\n\tsort?: { [key: string]: SortDirection };\n\t\u002F\u002F Name for lastModifiedBy field\n\tname?: string;\n}\n```\n\nAgenda uses [Human Interval](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fhuman-interval) for specifying the intervals. It supports the following units:\n\n`seconds`, `minutes`, `hours`, `days`,`weeks`, `months` -- assumes 30 days, `years` -- assumes 365 days\n\nMore sophisticated examples\n\n```js\nagenda.processEvery('one minute');\nagenda.processEvery('1.5 minutes');\nagenda.processEvery('3 days and 4 hours');\nagenda.processEvery('3 days, 4 hours and 36 seconds');\n```\n\n### Backend Configuration\n\nAgenda uses a pluggable backend system. The backend provides storage and optionally real-time notifications.\n\n**Using MongoBackend:**\n\n```js\nimport { Agenda } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\n\u002F\u002F Via connection string\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ address: 'mongodb:\u002F\u002Flocalhost:27017\u002Fagenda-test' })\n});\n\n\u002F\u002F Via existing MongoDB connection\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ mongo: mongoClientInstance.db('agenda-test') })\n});\n\n\u002F\u002F With custom collection name\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({\n\t\taddress: 'mongodb:\u002F\u002Flocalhost:27017\u002Fagenda-test',\n\t\tcollection: 'myJobs'\n\t})\n});\n\u002F\u002F MongoBackend provides both storage AND real-time notifications via MongoDB Change Streams (Native)\n```\n\n**Using PostgresBackend:**\n\n```bash\nnpm install @agendajs\u002Fpostgres-backend\n```\n\n```js\nimport { Agenda } from 'agenda';\nimport { PostgresBackend } from '@agendajs\u002Fpostgres-backend';\n\nconst agenda = new Agenda({\n\tbackend: new PostgresBackend({\n\t\tconnectionString: 'postgresql:\u002F\u002Fuser:pass@localhost:5432\u002Fmydb'\n\t})\n});\n\u002F\u002F PostgresBackend provides both storage AND real-time notifications via LISTEN\u002FNOTIFY\n```\n\n**Using RedisBackend:**\n\n```bash\nnpm install @agendajs\u002Fredis-backend\n```\n\n```js\nimport { Agenda } from 'agenda';\nimport { RedisBackend } from '@agendajs\u002Fredis-backend';\n\nconst agenda = new Agenda({\n\tbackend: new RedisBackend({\n\t\tconnectionString: 'redis:\u002F\u002Flocalhost:6379'\n\t})\n});\n\u002F\u002F RedisBackend provides both storage AND real-time notifications via Pub\u002FSub\n```\n\n**Custom backend:**\n\nYou can implement a custom backend by implementing the `AgendaBackend` interface. See [Custom Database Driver](docs\u002Fcustom-database-driver.md) for details.\n\n```js\nconst agenda = new Agenda({ backend: myCustomBackend });\n```\n\nAgenda will emit a `ready` event (see [Agenda Events](#agenda-events)) when properly connected to the backend.\nIt is safe to call `agenda.start()` without waiting for this event, as this is handled internally.\n\n### Real-Time Notifications\n\nBy default, Agenda uses periodic polling (controlled by `processEvery`) to check for new jobs. For faster job processing in distributed environments, you can configure a notification channel that triggers immediate job processing when jobs are created or updated.\n\n**Using the built-in InMemoryNotificationChannel (single process):**\n\n```js\nimport { Agenda, InMemoryNotificationChannel } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ mongo: db }),\n\tprocessEvery: '30 seconds', \u002F\u002F Fallback polling interval\n\tnotificationChannel: new InMemoryNotificationChannel()\n});\n```\n\n**Using the fluent API:**\n\n```js\nconst channel = new InMemoryNotificationChannel();\nconst agenda = new Agenda({ backend: new MongoBackend({ mongo: db }) })\n\t.notifyVia(channel);\n```\n\nThe `InMemoryNotificationChannel` is useful for testing and single-process deployments. For multi-process or distributed deployments, you can implement custom notification channels using Redis pub\u002Fsub, PostgreSQL LISTEN\u002FNOTIFY, or other messaging systems.\n\n**Using the MongoDB Change Streams (Native):**\n\nIf your MongoDB deployment is a replica set (even a single-node replica set works), you can use MongoChangeStreamNotificationChannel for native real-time notifications without any external dependencies:\n\nPlease refer [this](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fagenda\u002Fblob\u002Fmain\u002Fpackages\u002Fmongo-backend\u002FREADME.md#option-1-mongodb-change-streams-native) for more details , you can find more about Mongo backend in  [this](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fagenda\u002Fblob\u002Fmain\u002Fpackages\u002Fmongo-backend\u002FREADME.md) file\n\n**Unified backend with notifications:**\n\nA backend can provide both storage AND notifications. For example, a PostgreSQL backend could use LISTEN\u002FNOTIFY:\n\n```js\n\u002F\u002F PostgresBackend implements both repository and notificationChannel\nconst agenda = new Agenda({\n\tbackend: new PostgresBackend({ connectionString: 'postgres:\u002F\u002F...' })\n\t\u002F\u002F No need for separate notificationChannel - PostgresBackend provides it!\n});\n```\n\n**Mixing backends (storage from one system, notifications from another):**\n\n```js\n\u002F\u002F MongoDB for storage, Redis for notifications\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ mongo: db }),\n\tnotificationChannel: new RedisNotificationChannel({ url: 'redis:\u002F\u002F...' })\n});\n```\n\n**Implementing a custom notification channel:**\n\nExtend `BaseNotificationChannel` or implement `NotificationChannel`:\n\n```ts\nimport { BaseNotificationChannel, JobNotification } from 'agenda';\n\nclass RedisNotificationChannel extends BaseNotificationChannel {\n\tasync connect(): Promise\u003Cvoid> {\n\t\t\u002F\u002F Connect to Redis\n\t\tthis.setState('connected');\n\t}\n\n\tasync disconnect(): Promise\u003Cvoid> {\n\t\t\u002F\u002F Disconnect from Redis\n\t\tthis.setState('disconnected');\n\t}\n\n\tasync publish(notification: JobNotification): Promise\u003Cvoid> {\n\t\t\u002F\u002F Publish to Redis channel\n\t}\n}\n```\n\nThe notification channel is automatically connected when `agenda.start()` is called and disconnected when `agenda.stop()` is called.\n\n### name(name)\n\nSets the `lastModifiedBy` field to `name` in the jobs collection.\nUseful if you have multiple job processors (agendas) and want to see which\njob queue last ran the job.\n\n```js\nagenda.name(os.hostname + '-' + process.pid);\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ name: 'test queue' });\n```\n\n### processEvery(interval)\n\nTakes a string `interval` which can be either a traditional javascript number,\nor a string such as `3 minutes`\n\nSpecifies the frequency at which agenda will query the database looking for jobs\nthat need to be processed. Agenda internally uses `setTimeout` to guarantee that\njobs run at (close to ~3ms) the right time.\n\nDecreasing the frequency will result in fewer database queries, but more jobs\nbeing stored in memory.\n\nAlso worth noting is that if the job queue is shutdown, any jobs stored in memory\nthat haven't run will still be locked, meaning that you may have to wait for the\nlock to expire. By default it is `'5 seconds'`.\n\n```js\nagenda.processEvery('1 minute');\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ processEvery: '30 seconds' });\n```\n\n### maxConcurrency(number)\n\nTakes a `number` which specifies the max number of jobs that can be running at\nany given moment. By default it is `20`.\n\n```js\nagenda.maxConcurrency(20);\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ maxConcurrency: 20 });\n```\n\n### defaultConcurrency(number)\n\nTakes a `number` which specifies the default number of a specific job that can be running at\nany given moment. By default it is `5`.\n\n```js\nagenda.defaultConcurrency(5);\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ defaultConcurrency: 5 });\n```\n\n### lockLimit(number)\n\nTakes a `number` which specifies the max number jobs that can be locked at any given moment. By default it is `0` for no max.\n\n```js\nagenda.lockLimit(0);\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ lockLimit: 0 });\n```\n\n### defaultLockLimit(number)\n\nTakes a `number` which specifies the default number of a specific job that can be locked at any given moment. By default it is `0` for no max.\n\n```js\nagenda.defaultLockLimit(0);\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ defaultLockLimit: 0 });\n```\n\n### defaultLockLifetime(number)\n\nTakes a `number` which specifies the default lock lifetime in milliseconds. By\ndefault it is 10 minutes. This can be overridden by specifying the\n`lockLifetime` option to a defined job.\n\nA job will unlock if it is finished (ie. the returned Promise resolves\u002Frejects\nor `done` is specified in the params and `done()` is called) before the\n`lockLifetime`. The lock is useful if the job crashes or times out.\n\n```js\nagenda.defaultLockLifetime(10000);\n```\n\nYou can also specify it during instantiation\n\n```js\nconst agenda = new Agenda({ defaultLockLifetime: 10000 });\n```\n\n## Agenda Events\n\nAn instance of an agenda will emit the following events:\n\n- `ready` - called when Agenda mongo connection is successfully opened and indices created.\n  If you're passing agenda an existing connection, you shouldn't need to listen for this, as `agenda.start()` will not resolve until indices have been created.\n  If you're using the `db` options, or call `database`, then you may still need to listen for the `ready` event before saving jobs. `agenda.start()` will still wait for the connection to be opened.\n- `error` - called when Agenda mongo connection process has thrown an error\n\n```js\nawait agenda.start();\n```\n\n## Defining Job Processors\n\nBefore you can use a job, you must define its processing behavior.\n\n### define(jobName, fn, [options])\n\nDefines a job with the name of `jobName`. When a job of `jobName` gets run, it\nwill be passed to `fn(job, done)`. To maintain asynchronous behavior, you may\neither provide a Promise-returning function in `fn` _or_ provide `done` as a\nsecond parameter to `fn`. If `done` is specified in the function signature, you\nmust call `done()` when you are processing the job. If your function is\nsynchronous or returns a Promise, you may omit `done` from the signature.\n\n`options` is an optional argument which can overwrite the defaults. It can take\nthe following:\n\n- `concurrency`: `number` maximum number of that job that can be running at once (per instance of agenda)\n- `lockLimit`: `number` maximum number of that job that can be locked at once (per instance of agenda)\n- `lockLifetime`: `number` interval in ms of how long the job stays locked for (see [multiple job processors](#multiple-job-processors) for more info).\n  A job will automatically unlock once a returned promise resolves\u002Frejects (or if `done` is specified in the signature and `done()` is called).\n- `priority`: `(lowest|low|normal|high|highest|number)` specifies the priority\n  of the job. Higher priority jobs will run first. See the priority mapping\n  below\n- `backoff`: `BackoffStrategy` a function that determines retry delay on failure. See [Automatic Retry with Backoff](#automatic-retry-with-backoff) for details\n- `removeOnComplete`: `boolean` automatically remove the job from the database after successful completion (one-time jobs only). Overrides the global `removeOnComplete` setting. See [Auto-Cleanup of Completed Jobs](#auto-cleanup-of-completed-jobs) for details\n- `logging`: `boolean` override whether this job type's lifecycle events are persisted. See [Persistent Job Logging](#persistent-job-logging)\n\nPriority mapping:\n\n```\n{\n  highest: 20,\n  high: 10,\n  normal: 0,\n  low: -10,\n  lowest: -20\n}\n```\n\nAsync Job:\n\n```js\nagenda.define('some long running job', async job => {\n\tconst data = await doSomelengthyTask();\n\tawait formatThatData(data);\n\tawait sendThatData(data);\n});\n```\n\nAsync Job (using `done`):\n\n```js\nagenda.define('some long running job', (job, done) => {\n\tdoSomelengthyTask(data => {\n\t\tformatThatData(data);\n\t\tsendThatData(data);\n\t\tdone();\n\t});\n});\n```\n\nSync Job:\n\n```js\nagenda.define('say hello', job => {\n\tconsole.log('Hello!');\n});\n```\n\n`define()` acts like an assignment: if `define(jobName, ...)` is called multiple times (e.g. every time your script starts), the definition in the last call will overwrite the previous one. Thus, if you `define` the `jobName` only once in your code, it's safe for that call to execute multiple times.\n\n## Automatic Retry with Backoff\n\nAgenda supports automatic retry with configurable backoff strategies. When a job fails, it can be automatically rescheduled based on the backoff strategy you define.\n\n### Basic Usage\n\n```js\nimport { Agenda, backoffStrategies } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ address: 'mongodb:\u002F\u002Flocalhost\u002Fagenda' })\n});\n\n\u002F\u002F Define a job with exponential backoff\nagenda.define(\n\t'send email',\n\tasync job => {\n\t\tawait sendEmail(job.attrs.data);\n\t},\n\t{\n\t\tbackoff: backoffStrategies.exponential({\n\t\t\tdelay: 1000,      \u002F\u002F Start with 1 second\n\t\t\tmaxRetries: 5,    \u002F\u002F Retry up to 5 times\n\t\t\tfactor: 2,        \u002F\u002F Double the delay each time\n\t\t\tjitter: 0.1       \u002F\u002F Add 10% randomness to prevent thundering herd\n\t\t})\n\t}\n);\n\u002F\u002F Retries at: ~1s, ~2s, ~4s, ~8s, ~16s (then gives up)\n```\n\n### Built-in Backoff Strategies\n\nAgenda provides three built-in backoff strategies:\n\n#### Constant Backoff\n\nSame delay between each retry attempt.\n\n```js\nimport { constant } from 'agenda';\n\nagenda.define('my-job', handler, {\n\tbackoff: constant({\n\t\tdelay: 5000,      \u002F\u002F 5 seconds between each retry\n\t\tmaxRetries: 3     \u002F\u002F Retry up to 3 times\n\t})\n});\n\u002F\u002F Retries at: 5s, 5s, 5s\n```\n\n#### Linear Backoff\n\nDelay increases by a fixed amount each retry.\n\n```js\nimport { linear } from 'agenda';\n\nagenda.define('my-job', handler, {\n\tbackoff: linear({\n\t\tdelay: 1000,      \u002F\u002F Start with 1 second\n\t\tincrement: 2000,  \u002F\u002F Add 2 seconds each retry (default: same as delay)\n\t\tmaxRetries: 4,\n\t\tmaxDelay: 10000   \u002F\u002F Cap at 10 seconds\n\t})\n});\n\u002F\u002F Retries at: 1s, 3s, 5s, 7s\n```\n\n#### Exponential Backoff\n\nDelay multiplies by a factor each retry. Best for rate-limited APIs.\n\n```js\nimport { exponential } from 'agenda';\n\nagenda.define('my-job', handler, {\n\tbackoff: exponential({\n\t\tdelay: 100,       \u002F\u002F Start with 100ms\n\t\tfactor: 2,        \u002F\u002F Double each time (default: 2)\n\t\tmaxRetries: 5,\n\t\tmaxDelay: 30000,  \u002F\u002F Cap at 30 seconds\n\t\tjitter: 0.2       \u002F\u002F Add 20% randomness\n\t})\n});\n\u002F\u002F Retries at: ~100ms, ~200ms, ~400ms, ~800ms, ~1600ms\n```\n\n### Preset Strategies\n\nFor common use cases, Agenda provides preset strategies:\n\n```js\nimport { backoffStrategies } from 'agenda';\n\n\u002F\u002F Aggressive: Fast retries for transient failures\n\u002F\u002F 100ms, 200ms, 400ms (3 retries in ~700ms)\nagenda.define('quick-job', handler, {\n\tbackoff: backoffStrategies.aggressive()\n});\n\n\u002F\u002F Standard: Balanced approach (default recommendation)\n\u002F\u002F ~1s, ~2s, ~4s, ~8s, ~16s with 10% jitter (5 retries)\nagenda.define('normal-job', handler, {\n\tbackoff: backoffStrategies.standard()\n});\n\n\u002F\u002F Relaxed: Gentle backoff for rate-limited APIs\n\u002F\u002F ~5s, ~15s, ~45s, ~135s with 10% jitter (4 retries)\nagenda.define('api-job', handler, {\n\tbackoff: backoffStrategies.relaxed()\n});\n```\n\n### Custom Backoff Functions\n\nYou can define your own backoff logic by providing a function:\n\n```js\nagenda.define('custom-job', handler, {\n\tbackoff: (context) => {\n\t\t\u002F\u002F context contains: { attempt, error, jobName, jobData }\n\n\t\t\u002F\u002F Return delay in milliseconds, or null to stop retrying\n\t\tif (context.attempt > 3) return null;\n\n\t\t\u002F\u002F Fibonacci-like sequence\n\t\tconst fibDelays = [1000, 1000, 2000, 3000, 5000];\n\t\treturn fibDelays[context.attempt - 1];\n\t}\n});\n```\n\n### Combining Strategies\n\nUse `combine()` to chain multiple strategies:\n\n```js\nimport { combine, constant, exponential } from 'agenda';\n\nagenda.define('complex-job', handler, {\n\tbackoff: combine(\n\t\t\u002F\u002F First 2 retries: quick constant delay\n\t\t(ctx) => ctx.attempt \u003C= 2 ? 100 : null,\n\t\t\u002F\u002F Then switch to exponential\n\t\t(ctx) => {\n\t\t\tif (ctx.attempt > 5) return null;\n\t\t\treturn 1000 * Math.pow(2, ctx.attempt - 3);\n\t\t}\n\t)\n});\n```\n\n### Conditional Retry\n\nUse `when()` to retry only for specific errors:\n\n```js\nimport { when, exponential } from 'agenda';\n\nagenda.define('api-job', handler, {\n\tbackoff: when(\n\t\t\u002F\u002F Only retry on timeout or rate limit errors\n\t\t(ctx) =>\n\t\t\tctx.error.message.includes('timeout') ||\n\t\t\tctx.error.message.includes('rate limit'),\n\t\texponential({ delay: 1000, maxRetries: 3 })\n\t)\n});\n```\n\n### Retry Events\n\nListen for retry events to monitor job behavior:\n\n```js\n\u002F\u002F When a job is scheduled for retry\nagenda.on('retry', (job, details) => {\n\tconsole.log(`Job ${job.attrs.name} retry #${details.attempt}`);\n\tconsole.log(`  Next run: ${details.nextRunAt}`);\n\tconsole.log(`  Delay: ${details.delay}ms`);\n\tconsole.log(`  Error: ${details.error.message}`);\n});\n\n\u002F\u002F Job-specific retry event\nagenda.on('retry:send email', (job, details) => {\n\tmetrics.increment('email.retries');\n});\n\n\u002F\u002F When all retries are exhausted\nagenda.on('retry exhausted', (error, job) => {\n\tconsole.log(`Job ${job.attrs.name} failed after ${job.attrs.failCount} attempts`);\n\talertOps(job, error);\n});\n\n\u002F\u002F Job-specific exhaustion\nagenda.on('retry exhausted:critical-job', (error, job) => {\n\t\u002F\u002F Move to dead letter queue, send alert, etc.\n});\n```\n\n### Backoff Options Reference\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `delay` | number | 1000 | Initial delay in milliseconds |\n| `maxRetries` | number | 3 | Maximum retry attempts |\n| `maxDelay` | number | Infinity | Maximum delay cap |\n| `jitter` | number | 0 | Randomness factor (0-1) |\n| `factor` | number | 2 | Multiplier for exponential backoff |\n| `increment` | number | delay | Amount to add for linear backoff |\n\n### Important Notes\n\n- **failCount tracks attempts**: Use `job.attrs.failCount` to see how many times a job has failed\n- **Backoff is per-definition**: Set the backoff strategy when defining the job, not when scheduling it\n- **Repeating jobs can use backoff**: If a repeating job (created with `every()`) has a backoff configured and fails, it will retry immediately rather than waiting for the next scheduled run\n- **Manual retry still works**: You can still listen to `fail` events and manually reschedule if needed\n\n## Job Debouncing\n\nDebouncing allows you to combine multiple rapid job submissions into a single execution. This is useful for scenarios like:\n- Updating a search index after rapid document changes\n- Syncing user data after multiple rapid updates\n- Rate-limiting notifications\n\n### How It Works\n\nDebouncing uses the `unique()` constraint combined with a `.debounce()` modifier. When multiple saves occur for the same unique key within the debounce window, only one job execution happens.\n\n```\nTimeline: job.save() calls for same unique key\n          ↓       ↓       ↓\n          T=0     T=2s    T=4s                 T=9s\n\nTRAILING (default):\n  nextRunAt: 5s  →  7s  →  9s        executes→ ✓\n  Effect: Waits for \"quiet period\", runs once at end\n\nLEADING:\n  nextRunAt: 0   →  0   →  0         executes→ ✓ (at T=0)\n  Effect: Runs immediately on first call, ignores rest during window\n```\n\n### Basic Usage\n\n```js\nimport { Agenda } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ address: 'mongodb:\u002F\u002Flocalhost\u002Fagenda' })\n});\n\n\u002F\u002F Debounce job - execute 2s after last save\nawait agenda.create('updateSearchIndex', { entityType: 'products' })\n  .unique({ 'data.entityType': 'products' })\n  .debounce(2000)\n  .save();\n\n\u002F\u002F Multiple rapid calls → single execution after 2s quiet period\nfor (const change of rapidChanges) {\n  await agenda.create('updateSearchIndex', { entityType: 'products', change })\n    .unique({ 'data.entityType': 'products' })\n    .debounce(2000)\n    .save();\n}\n\u002F\u002F → Executes once with the last change's data\n```\n\n### Debounce Strategies\n\n#### Trailing (Default)\n\nThe job executes after a quiet period. Each save resets the timer.\n\n```js\nawait agenda.create('syncUserActivity', { userId: 123 })\n  .unique({ 'data.userId': 123 })\n  .debounce(5000)  \u002F\u002F Wait 5s after last save\n  .save();\n```\n\n#### Leading\n\nThe job executes immediately on first call. Subsequent calls within the window are ignored.\n\n```js\nawait agenda.create('sendNotification', { channel: '#alerts' })\n  .unique({ 'data.channel': '#alerts' })\n  .debounce(60000, { strategy: 'leading' })\n  .save();\n\u002F\u002F → First call executes immediately, subsequent calls within 60s are ignored\n```\n\n### maxWait Option\n\nWith trailing strategy, `maxWait` guarantees execution within a maximum time even if saves keep coming.\n\n```js\nawait agenda.create('syncUserActivity', { userId: 123 })\n  .unique({ 'data.userId': 123 })\n  .debounce(5000, { maxWait: 30000 })\n  .save();\n\u002F\u002F → Even with continuous saves, job runs within 30s\n```\n\n### Debounce Options Reference\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `delay` | number | - | Debounce window in milliseconds (required, first argument) |\n| `strategy` | `'trailing'` \\| `'leading'` | `'trailing'` | When to execute the job |\n| `maxWait` | number | - | Max time before forced execution (trailing only) |\n\n### Requirements\n\n- **Requires `unique()` constraint**: Debounce identifies which jobs to combine using the unique key\n- **Without `unique()`**: Each save creates a new job (no debouncing occurs)\n- **Persistence**: Debounce state is stored in the database, surviving process restarts\n\n### Helper Method: nowDebounced()\n\nFor convenience, use `nowDebounced()` to create a debounced job in one call:\n\n```js\n\u002F\u002F Equivalent to create().unique().debounce().save()\nawait agenda.nowDebounced(\n  'updateSearchIndex',\n  { entityType: 'products' },\n  { 'data.entityType': 'products' },  \u002F\u002F unique query\n  { delay: 2000 }                      \u002F\u002F debounce options\n);\n```\n\n## Auto-Cleanup of Completed Jobs\n\nBy default, one-time jobs remain in the database after completion. If you want to automatically remove them after they succeed, use the `removeOnComplete` option.\n\n### Global Setting\n\nSet `removeOnComplete` in the Agenda constructor to apply to all jobs:\n\n```js\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({ address: 'mongodb:\u002F\u002Flocalhost\u002Fagenda' }),\n\tremoveOnComplete: true\n});\n```\n\nWith this setting, any one-time job (i.e. a job with no `nextRunAt` after completion) will be removed from the database after it succeeds. Recurring jobs are never removed, and failed jobs are always kept.\n\n### Per-Job Override\n\nYou can override the global setting for specific job types via `define()`:\n\n```js\n\u002F\u002F Global removeOnComplete is false (default), but this job type opts in\nagenda.define('send-welcome-email', async job => {\n\tawait sendEmail(job.attrs.data.to);\n}, { removeOnComplete: true });\n\n\u002F\u002F Global removeOnComplete is true, but this job type opts out\nagenda.define('audit-log', async job => {\n\tawait writeAuditLog(job.attrs.data);\n}, { removeOnComplete: false });\n```\n\n### Behavior Details\n\n- **Only one-time jobs**: Recurring jobs (created with `every()`) are never removed, since they always have a `nextRunAt`\n- **Only on success**: Failed jobs are always kept in the database regardless of the setting\n- **Events fire first**: The `complete` and `success` events are emitted before the job is removed, so listeners can still access job data\n- **Safe removal**: If the removal fails (e.g. due to a database error), the error is logged but does not affect the processing loop\n\n## Persistent Job Logging\n\nAgenda can persist structured job lifecycle events (start, success, fail, complete, retry, etc.) to the backend's database. This is useful for auditing, debugging, and monitoring — events can be queried programmatically via `agenda.getLogs()` or viewed in [Agendash](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fagenda\u002Ftree\u002Fmain\u002Fpackages\u002Fagendash).\n\nLogging is **disabled by default** and must be explicitly enabled via the `logging` option.\n\n### Enabling Logging\n\n```typescript\nimport { Agenda } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\n\u002F\u002F Enable — log all jobs using the backend's built-in logger\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  logging: true\n});\n\n\u002F\u002F Enable — opt-in per job (logger active, but jobs are NOT logged by default)\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  logging: { default: false }\n});\nagenda.define('important', handler, { logging: true });  \u002F\u002F this job IS logged\nagenda.define('noisy', handler);                         \u002F\u002F this job is NOT logged\n\n\u002F\u002F Enable — custom logger (e.g., log to Postgres while using Mongo for storage)\nimport { PostgresJobLogger } from '@agendajs\u002Fpostgres-backend';\nconst pgLogger = new PostgresJobLogger({ pool: myPool });\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  logging: pgLogger\n});\n\n\u002F\u002F Enable — custom logger + opt-in per job\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  logging: { logger: pgLogger, default: false }\n});\n```\n\n### `logging` Option Values\n\n| Value | Logger used | Jobs logged by default |\n|-------|-------------|----------------------|\n| `false` \u002F omitted | none | n\u002Fa |\n| `true` | backend's built-in | all |\n| `JobLogger` instance | the provided logger | all |\n| `{ default: false }` | backend's built-in | none (opt-in per job) |\n| `{ logger: JobLogger }` | the provided logger | all |\n| `{ logger: JobLogger, default: false }` | the provided logger | none (opt-in per job) |\n\n### Per-Job Definition Control\n\nEach job definition can override the global default:\n\n```typescript\nagenda.define('important-job', handler, { logging: true });   \u002F\u002F always logged\nagenda.define('noisy-job', handler, { logging: false });      \u002F\u002F never logged\nagenda.define('default-job', handler);                        \u002F\u002F follows global default\n```\n\n### Querying Logs\n\n```typescript\n\u002F\u002F Get recent logs for a specific job\nconst { entries, total } = await agenda.getLogs({\n  jobName: 'myJob',\n  limit: 100,\n  sort: 'desc'\n});\n\n\u002F\u002F Filter by level, event, time range\nconst { entries } = await agenda.getLogs({\n  level: 'error',\n  event: ['fail', 'retry:exhausted'],\n  from: new Date('2025-01-01'),\n  to: new Date()\n});\n\n\u002F\u002F Clear old logs\nawait agenda.clearLogs({ to: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) });\n```\n\n### Cross-Backend Logging\n\nEach backend provides a standalone `JobLogger` that can be used independently. This lets you store jobs in one backend while logging to another:\n\n```typescript\nimport { RedisJobLogger } from '@agendajs\u002Fredis-backend';\nimport Redis from 'ioredis';\n\nconst logger = new RedisJobLogger({ redis: new Redis('redis:\u002F\u002Flocalhost:6379') });\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  logging: logger\n});\n```\n\nAvailable standalone loggers:\n- `MongoJobLogger` from `@agendajs\u002Fmongo-backend` — stores in a MongoDB collection (`agenda_logs`)\n- `PostgresJobLogger` from `@agendajs\u002Fpostgres-backend` — stores in a PostgreSQL table (`agenda_logs`)\n- `RedisJobLogger` from `@agendajs\u002Fredis-backend` — stores in Redis sorted sets + hashes\n\n### Log Events\n\nThe following lifecycle events are recorded:\n\n| Event | Level | Description |\n|-------|-------|-------------|\n| `start` | info | Job started executing |\n| `success` | info | Job completed successfully |\n| `fail` | error | Job threw an error |\n| `complete` | info | Job finished (regardless of outcome) |\n| `retry` | warn | Job scheduled for retry after failure |\n| `retry:exhausted` | error | All retry attempts exhausted |\n| `locked` | debug | Job was locked for processing |\n| `expired` | warn | Job lock expired (timed out) |\n\n## Creating Jobs\n\n### every(interval, name, [data], [options])\n\nRuns job `name` at the given `interval`. Optionally, data and options can be passed in.\nEvery creates a job of type `single`, which means that it will only create one\njob in the database, even if that line is run multiple times. This lets you put\nit in a file that may get run multiple times, such as `webserver.js` which may\nreboot from time to time.\n\n`interval` can be a human-readable format `String`, a [cron format](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fcron-parser) `String`, or a `Number`.\n\n`data` is an optional argument that will be passed to the processing function\nunder `job.attrs.data`.\n\n`options` is an optional argument containing:\n\n- `timezone`: Timezone for cron expressions (e.g., `'America\u002FNew_York'`)\n- `skipImmediate`: If `true`, skip the immediate first run\n- `forkMode`: If `true`, run in a forked child process\n- `startDate`: `Date` or string - job won't run before this date\n- `endDate`: `Date` or string - job won't run after this date\n- `skipDays`: Array of days to skip (0=Sunday, 1=Monday, ..., 6=Saturday)\n\nIn order to use this argument, `data` must also be specified.\n\nReturns the `job`.\n\n```js\nagenda.define('printAnalyticsReport', async job => {\n\tconst users = await User.doSomethingReallyIntensive();\n\tprocessUserData(users);\n\tconsole.log('I print a report!');\n});\n\nagenda.every('15 minutes', 'printAnalyticsReport');\n```\n\n**With date constraints (business hours only, weekdays):**\n\n```js\nawait agenda.every('1 hour', 'business-metrics', { type: 'hourly' }, {\n\tstartDate: new Date('2024-06-01'),\n\tendDate: new Date('2024-12-31'),\n\tskipDays: [0, 6],  \u002F\u002F Skip weekends\n\ttimezone: 'America\u002FNew_York'\n});\n```\n\nOptionally, `name` could be array of job names, which is convenient for scheduling\ndifferent jobs for same `interval`.\n\n```js\nagenda.every('15 minutes', ['printAnalyticsReport', 'sendNotifications', 'updateUserRecords']);\n```\n\nIn this case, `every` returns array of `jobs`.\n\n### schedule(when, name, [data], [options])\n\nSchedules a job to run `name` once at a given time. `when` can be a `Date` or a\n`String` such as `tomorrow at 5pm`.\n\n`data` is an optional argument that will be passed to the processing function\nunder `job.attrs.data`.\n\n`options` is an optional argument containing:\n\n- `startDate`: `Date` or string - job won't run before this date\n- `endDate`: `Date` or string - job won't run after this date (sets `nextRunAt` to `null`)\n- `skipDays`: Array of days to skip (0=Sunday, 1=Monday, ..., 6=Saturday)\n\nReturns the `job`.\n\n```js\nagenda.schedule('tomorrow at noon', 'printAnalyticsReport', { userCount: 100 });\n```\n\n**With date constraints:**\n\n```js\n\u002F\u002F Schedule for Saturday, but skip weekends - will run on Monday instead\nawait agenda.schedule('next saturday', 'weekday-task', { id: 123 }, {\n\tskipDays: [0, 6]  \u002F\u002F Skip weekends\n});\n```\n\nOptionally, `name` could be array of job names, similar to the `every` method.\n\n```js\nagenda.schedule('tomorrow at noon', [\n\t'printAnalyticsReport',\n\t'sendNotifications',\n\t'updateUserRecords'\n]);\n```\n\nIn this case, `schedule` returns array of `jobs`.\n\n### now(name, [data])\n\nSchedules a job to run `name` once immediately.\n\n`data` is an optional argument that will be passed to the processing function\nunder `job.attrs.data`.\n\nReturns the `job`.\n\n```js\nagenda.now('do the hokey pokey');\n```\n\n### create(jobName, data)\n\nReturns an instance of a `jobName` with `data`. This does _NOT_ save the job in\nthe database. See below to learn how to manually work with jobs.\n\n```js\nconst job = agenda.create('printAnalyticsReport', { userCount: 100 });\nawait job.save();\nconsole.log('Job successfully saved');\n```\n\n## Managing Jobs\n\n### queryJobs(options)\n\nReturns jobs matching the given query options. Accepts a `JobsQueryOptions` object with the following fields:\n\n- `name` — Filter by job name\n- `names` — Filter by multiple job names\n- `state` — Filter by computed state (`'running'`, `'scheduled'`, `'queued'`, `'completed'`, `'failed'`, `'repeating'`, `'paused'`)\n- `id` \u002F `ids` — Filter by job ID(s)\n- `search` — Text search in job name\n- `data` — Filter by job data (exact or partial match)\n- `sort` — Sort options (e.g. `{ nextRunAt: 'asc', priority: 'desc' }`)\n- `limit` — Maximum number of jobs to return\n- `skip` — Number of jobs to skip (for pagination)\n\n```js\nconst { jobs, total } = await agenda.queryJobs({ name: 'printAnalyticsReport', limit: 3, skip: 1 });\n\u002F\u002F Work with jobs (see below)\n```\n\n### cancel(options)\n\nCancels any jobs matching the passed query options, and removes them from the database. Returns a Promise resolving to the number of cancelled jobs, or rejecting on error. At least one filter option must be provided — passing an empty object `cancel({})` is a safe no-op and removes nothing.\n\nOptions:\n- `id` \u002F `ids` — Match by job ID(s)\n- `name` \u002F `names` — Match by job name(s)\n- `notNames` — Exclude jobs matching these names\n- `data` — Match by job data (can be combined with `name` to narrow the filter)\n\n```js\nconst numRemoved = await agenda.cancel({ name: 'printAnalyticsReport' });\n\n\u002F\u002F Cancel only jobs with specific data\nconst numRemoved = await agenda.cancel({ name: 'sendEmail', data: { userId: 123 } });\n```\n\nThis functionality can also be achieved by first retrieving all the jobs from the database using `agenda.queryJobs()`, looping through the resulting array and calling `job.remove()` on each. It is however preferable to use `agenda.cancel()` for this use case, as this ensures the operation is atomic.\n\n### cancelAll()\n\nRemoves **all** jobs from the database unconditionally. Use with caution — this cannot be undone. Returns a Promise resolving to the number of removed jobs.\n\n```js\nconst numRemoved = await agenda.cancelAll();\n```\n\n### disable(options)\n\nDisables any jobs matching the passed query options, preventing any matching jobs from being run by the Job Processor. Accepts the same options as `cancel()`.\n\n```js\nconst numDisabled = await agenda.disable({ name: 'pollExternalService' });\n```\n\nSimilar to `agenda.cancel()`, this functionality can be achieved with a combination of `agenda.queryJobs()` and `job.disable()`\n\n### enable(options)\n\nEnables any jobs matching the passed query options, allowing any matching jobs to be run by the Job Processor. Accepts the same options as `cancel()`.\n\n```js\nconst numEnabled = await agenda.enable({ name: 'pollExternalService' });\n```\n\nSimilar to `agenda.cancel()`, this functionality can be achieved with a combination of `agenda.queryJobs()` and `job.enable()`\n\n### purge()\n\nRemoves all **orphaned** jobs — jobs whose names do not match any currently defined job. Useful if you rename a job definition and want to clean up old entries. Returns a Promise resolving to the number of removed jobs, or rejecting on error.\n\n_IMPORTANT:_ Do not run this before you finish defining all of your jobs, otherwise jobs without a definition will be removed.\n\n```js\nconst numRemoved = await agenda.purge();\n```\n\n## Starting the job processor\n\nTo get agenda to start processing jobs from the database you must start it. This\nwill schedule an interval (based on `processEvery`) to check for new jobs and\nrun them. You can also stop the queue.\n\n### start\n\nStarts the job queue processing, checking [`processEvery`](#processeveryinterval) time to see if there\nare new jobs. Must be called _after_ `processEvery`, and _before_ any job scheduling (e.g. `every`).\n\n### stop\n\nStops the job queue processing. Unlocks currently running jobs.\n\nThis can be very useful for graceful shutdowns so that currently running\u002Fgrabbed jobs are abandoned so that other\njob queues can grab them \u002F they are unlocked should the job queue start again. Here is an example of how to do a graceful\nshutdown.\n\n```js\nasync function graceful() {\n\tawait agenda.stop();\n\tprocess.exit(0);\n}\n\nprocess.on('SIGTERM', graceful);\nprocess.on('SIGINT', graceful);\n```\n\n### drain\n\nWaits for all currently running jobs to finish before stopping the job queue processing. Unlike `stop()`, this method does not unlock jobs - it lets them complete their work.\n\nThis is useful for graceful shutdowns where you want to ensure all in-progress work finishes before the process exits.\n\n```js\nasync function graceful() {\n\tawait agenda.drain();\n\tprocess.exit(0);\n}\n\nprocess.on('SIGTERM', graceful);\nprocess.on('SIGINT', graceful);\n```\n\n**With timeout** - useful when you need to shutdown within a time limit (e.g., cloud platforms like Heroku give 30 seconds):\n\n```js\nasync function graceful() {\n\tconst result = await agenda.drain(30000); \u002F\u002F 30 second timeout\n\tif (result.timedOut) {\n\t\tconsole.log(`Shutdown timeout: ${result.running} jobs still running`);\n\t}\n\tprocess.exit(0);\n}\n```\n\n**With AbortSignal** - for external control over the drain operation:\n\n```js\nconst controller = new AbortController();\n\n\u002F\u002F Abort drain after 30 seconds\nsetTimeout(() => controller.abort(), 30000);\n\nconst result = await agenda.drain({ signal: controller.signal });\nif (result.aborted) {\n\tconsole.log(`Drain aborted: ${result.running} jobs still running`);\n}\n```\n\n**DrainResult** - `drain()` returns information about what happened:\n\n```ts\ninterface DrainResult {\n\tcompleted: number;  \u002F\u002F jobs that finished during drain\n\trunning: number;    \u002F\u002F jobs still running (if timed out or aborted)\n\ttimedOut: boolean;  \u002F\u002F true if timeout was reached\n\taborted: boolean;   \u002F\u002F true if signal was aborted\n}\n```\n\n**Comparison of `stop()` vs `drain()`:**\n\n| Method | Running Jobs | New Jobs | Use Case |\n|--------|--------------|----------|----------|\n| `stop()` | Unlocks immediately | Stops accepting | Quick shutdown, jobs picked up by other workers |\n| `drain()` | Waits for completion | Stops accepting | Graceful shutdown, ensure work finishes |\n\n## Multiple job processors\n\nSometimes you may want to have multiple node instances \u002F machines process from\nthe same queue. Agenda supports a locking mechanism to ensure that multiple\nqueues don't process the same job.\n\nYou can configure the locking mechanism by specifying `lockLifetime` as an\ninterval when defining the job.\n\n```js\nagenda.define(\n\t'someJob',\n\t(job, cb) => {\n\t\t\u002F\u002F Do something in 10 seconds or less...\n\t},\n\t{ lockLifetime: 10000 }\n);\n```\n\nThis will ensure that no other job processor (this one included) attempts to run the job again\nfor the next 10 seconds. If you have a particularly long running job, you will want to\nspecify a longer lockLifetime.\n\nBy default it is 10 minutes. Typically you shouldn't have a job that runs for 10 minutes,\nso this is really insurance should the job queue crash before the job is unlocked.\n\nWhen a job is finished (i.e. the returned promise resolves\u002Frejects or `done` is\nspecified in the signature and `done()` is called), it will automatically unlock.\n\n## Manually working with a job\n\nA job instance has many instance methods. All mutating methods must be followed\nwith a call to `await job.save()` in order to persist the changes to the database.\n\n### repeatEvery(interval, [options])\n\nSpecifies an `interval` on which the job should repeat. The job runs at the time of defining as well in configured intervals, that is \"run _now_ and in intervals\".\n\n`interval` can be a human-readable format `String`, a [cron format](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fcron-parser) `String`, or a `Number`.\n\n`options` is an optional argument containing:\n\n`options.timezone`: should be a string as accepted by [moment-timezone](https:\u002F\u002Fmomentjs.com\u002Ftimezone\u002F) and is considered when using an interval in the cron string format.\n\n`options.skipImmediate`: `true` | `false` (default) Setting this `true` will skip the immediate run. The first run will occur only in configured interval.\n\n```js\njob.repeatEvery('10 minutes');\nawait job.save();\n```\n\n```js\njob.repeatEvery('3 minutes', {\n\tskipImmediate: true\n});\nawait job.save();\n```\n\n```js\njob.repeatEvery('0 6 * * *', {\n\ttimezone: 'America\u002FNew_York'\n});\nawait job.save();\n```\n\n### repeatAt(time)\n\nSpecifies a `time` when the job should repeat. [Possible values](https:\u002F\u002Fgithub.com\u002Fmatthewmueller\u002Fdate#examples)\n\n```js\njob.repeatAt('3:30pm');\nawait job.save();\n```\n\n### schedule(time)\n\nSpecifies the next `time` at which the job should run.\n\n```js\njob.schedule('tomorrow at 6pm');\nawait job.save();\n```\n\n### startDate(date)\n\nSets the start date for the job. The job will not run before this date. If `nextRunAt` is computed to be before `startDate`, it will be adjusted to `startDate`.\n\n```js\njob.startDate(new Date('2024-06-01'));\n\u002F\u002F Or with a string\njob.startDate('2024-06-01T00:00:00Z');\nawait job.save();\n```\n\n### endDate(date)\n\nSets the end date for the job. The job will not run after this date. If `nextRunAt` would be after `endDate`, it will be set to `null` and the job stops running.\n\n```js\njob.endDate(new Date('2024-12-31'));\n\u002F\u002F Or with a string\njob.endDate('2024-12-31T23:59:59Z');\nawait job.save();\n```\n\n### skipDays(days)\n\nSets the days of the week to skip. The job will not run on these days. Days are specified as an array of numbers where 0 = Sunday, 1 = Monday, ..., 6 = Saturday.\n\n```js\n\u002F\u002F Skip weekends\njob.skipDays([0, 6]);\nawait job.save();\n```\n\n```js\n\u002F\u002F Skip Monday and Wednesday\njob.skipDays([1, 3]);\nawait job.save();\n```\n\n**Combining date constraints:**\n\n```js\nconst job = agenda.create('business-report', { type: 'daily' });\njob.startDate('2024-06-01')\n   .endDate('2024-12-31')\n   .skipDays([0, 6])  \u002F\u002F Skip weekends\n   .repeatEvery('1 day', { timezone: 'America\u002FNew_York' });\nawait job.save();\n```\n\n### priority(priority)\n\nSpecifies the `priority` weighting of the job. Can be a number or a string from\nthe above priority table.\n\n```js\njob.priority('low');\nawait job.save();\n```\n\n### unique(properties, [options])\n\nEnsure that only one instance of this job exists with the specified properties\n\n`options` is an optional argument which can overwrite the defaults. It can take\nthe following:\n\n- `insertOnly`: `boolean` will prevent any properties from persisting if the job already exists. Defaults to false.\n\n```js\njob.unique({ 'data.type': 'active', 'data.userId': '123', nextRunAt: date });\nawait job.save();\n```\n\n_IMPORTANT:_ To avoid high CPU usage by MongoDB, make sure to create an index on the used fields, like `data.type` and `data.userId` for the example above.\n\n### debounce(delay, [options])\n\nConfigures debouncing for the job. Requires a `unique()` constraint to be set. See [Job Debouncing](#job-debouncing) for detailed documentation.\n\n`delay` is the debounce window in milliseconds.\n\n`options` is an optional argument:\n- `strategy`: `'trailing'` (default) or `'leading'` - when to execute the job\n- `maxWait`: number - maximum time before forced execution (trailing only)\n\n```js\njob.unique({ 'data.userId': 123 });\njob.debounce(5000);  \u002F\u002F 5 second debounce\nawait job.save();\n\n\u002F\u002F With options\njob.unique({ 'data.channel': '#alerts' });\njob.debounce(60000, { strategy: 'leading' });\nawait job.save();\n```\n\n### fail(reason)\n\nSets `job.attrs.failedAt` to `now`, and sets `job.attrs.failReason` to `reason`.\n\nOptionally, `reason` can be an error, in which case `job.attrs.failReason` will\nbe set to `error.message`\n\n```js\njob.fail('insufficient disk space');\n\u002F\u002F or\njob.fail(new Error('insufficient disk space'));\nawait job.save();\n```\n\n### run(callback)\n\nRuns the given `job` and calls `callback(err, job)` upon completion. Normally\nyou never need to call this manually.\n\n```js\njob.run((err, job) => {\n\tconsole.log(\"I don't know why you would need to do this...\");\n});\n```\n\n### save()\n\nSaves the `job.attrs` into the database. Returns a Promise resolving to a Job instance, or rejecting on error.\n\n```js\ntry {\n\tawait job.save();\n\tcosole.log('Successfully saved job to collection');\n} catch (e) {\n\tconsole.error('Error saving job to collection');\n}\n```\n\n### remove()\n\nRemoves the `job` from the database. Returns a Promise resolving to the number of jobs removed, or rejecting on error.\n\n```js\ntry {\n\tawait job.remove();\n\tconsole.log('Successfully removed job from collection');\n} catch (e) {\n\tconsole.error('Error removing job from collection');\n}\n```\n\n### disable()\n\nDisables the `job`. Upcoming runs won't execute.\n\n### enable()\n\nEnables the `job` if it got disabled before. Upcoming runs will execute.\n\n### touch()\n\nResets the lock on the job. Useful to indicate that the job hasn't timed out\nwhen you have very long running jobs. The call returns a promise that resolves\nwhen the job's lock has been renewed.\n\n```js\nagenda.define('super long job', async job => {\n\tawait doSomeLongTask();\n\tawait job.touch();\n\tawait doAnotherLongTask();\n\tawait job.touch();\n\tawait finishOurLongTasks();\n});\n```\n\n## Job Queue Events\n\nAn instance of an agenda will emit the following events:\n\n- `start` - called just before a job starts\n- `start:job name` - called just before the specified job starts\n\n```js\nagenda.on('start', job => {\n\tconsole.log('Job %s starting', job.attrs.name);\n});\n```\n\n- `complete` - called when a job finishes, regardless of if it succeeds or fails\n- `complete:job name` - called when a job finishes, regardless of if it succeeds or fails\n\n```js\nagenda.on('complete', job => {\n\tconsole.log(`Job ${job.attrs.name} finished`);\n});\n```\n\n- `success` - called when a job finishes successfully\n- `success:job name` - called when a job finishes successfully\n\n```js\nagenda.on('success:send email', job => {\n\tconsole.log(`Sent Email Successfully to ${job.attrs.data.to}`);\n});\n```\n\n- `fail` - called when a job throws an error\n- `fail:job name` - called when a job throws an error\n\n```js\nagenda.on('fail:send email', (err, job) => {\n\tconsole.log(`Job failed with error: ${err.message}`);\n});\n```\n\n- `retry` - called when a job is scheduled for automatic retry (requires backoff strategy)\n- `retry:job name` - called when a specific job is scheduled for retry\n\n```js\nagenda.on('retry', (job, details) => {\n\t\u002F\u002F details: { attempt, delay, nextRunAt, error }\n\tconsole.log(`Retrying ${job.attrs.name} in ${details.delay}ms (attempt ${details.attempt})`);\n});\n```\n\n- `retry exhausted` - called when a job has exhausted all retry attempts\n- `retry exhausted:job name` - called when a specific job exhausts retries\n\n```js\nagenda.on('retry exhausted:send email', (err, job) => {\n\tconsole.log(`Email job failed permanently after ${job.attrs.failCount} attempts`);\n});\n```\n\n## Frequently Asked Questions\n\n### What is the order in which jobs run?\n\nJobs are run with priority in a first in first out order (so they will be run in the order they were scheduled AND with respect to highest priority).\n\nFor example, if we have two jobs named \"send-email\" queued (both with the same priority), and the first job is queued at 3:00 PM and second job is queued at 3:05 PM with the same `priority` value, then the first job will run first if we start to send \"send-email\" jobs at 3:10 PM. However if the first job has a priority of `5` and the second job has a priority of `10`, then the second will run first (priority takes precedence) at 3:10 PM.\n\nThe default sort order is `{ nextRunAt: 'asc', priority: 'desc' }` and can be changed through the `sort` option when configuring the backend.\n\n### What is the difference between `lockLimit` and `maxConcurrency`?\n\nAgenda will lock jobs 1 by one, setting the `lockedAt` property in mongoDB, and creating an instance of the `Job` class which it caches into the `_lockedJobs` array. This defaults to having no limit, but can be managed using lockLimit. If all jobs will need to be run before agenda's next interval (set via `agenda.processEvery`), then agenda will attempt to lock all jobs.\n\nAgenda will also pull jobs from `_lockedJobs` and into `_runningJobs`. These jobs are actively being worked on by user code, and this is limited by `maxConcurrency` (defaults to 20).\n\nIf you have multiple instances of agenda processing the same job definition with a fast repeat time you may find they get unevenly loaded. This is because they will compete to lock as many jobs as possible, even if they don't have enough concurrency to process them. This can be resolved by tweaking the `maxConcurrency` and `lockLimit` properties.\n\n### Sample Project Structure?\n\nAgenda doesn't have a preferred project structure and leaves it to the user to\nchoose how they would like to use it. That being said, you can check out the\n[example project structure](#example-project-structure) below.\n\n### Web Interface?\n\nAgenda itself does not have a web interface built in but we do offer stand-alone web interface [Agendash](https:\u002F\u002Fgithub.com\u002Fagenda\u002Fagenda\u002Ftree\u002Fmain\u002Fpackages\u002Fagendash):\n\n\u003Ca href=\"https:\u002F\u002Fraw.githubusercontent.com\u002Fagenda\u002Fagenda\u002Fmain\u002Fpackages\u002Fagendash\u002Fagendash.png\">\u003Cimg src=\"https:\u002F\u002Fraw.githubusercontent.com\u002Fagenda\u002Fagenda\u002Fmain\u002Fpackages\u002Fagendash\u002Fagendash.png\" style=\"max-width:100%\" alt=\"Agendash interface\">\u003C\u002Fa>\n\n### Choosing a Backend\n\nAgenda v6 supports multiple storage backends. Choose based on your infrastructure:\n\n| Backend | Package | Best For |\n|---------|---------|----------|\n| **MongoDB** | `@agendajs\u002Fmongo-backend` | Default choice, excellent for most use cases. Strong consistency, flexible queries. |\n| **PostgreSQL** | `@agendajs\u002Fpostgres-backend` | Teams already using PostgreSQL. LISTEN\u002FNOTIFY provides real-time notifications without additional infrastructure. |\n| **Redis** | `@agendajs\u002Fredis-backend` | High-throughput scenarios. Fast Pub\u002FSub notifications. Configure persistence for durability. |\n\n**MongoDB** remains the default and most battle-tested backend. **PostgreSQL** is great when you want to consolidate on a single database. **Redis** offers the lowest latency for job notifications but requires proper persistence configuration (RDB\u002FAOF) for durability.\n\n#### Backend Capabilities\n\nEach backend provides different capabilities for storage and real-time notifications:\n\n| Backend | Storage | Notifications | Notes |\n|---------|:-------:|:-------------:|-------|\n| **MongoDB** (`MongoBackend`) | ✅ | ❌ | Storage only. Use with external notification channel for real-time. |\n| **MongoDB Change Streams** (`MongoChangeStreamNotificationChannel`) | ❌ | ✅ | Notification only. Requires MongoDB replica set. |\n| **PostgreSQL** (`PostgresBackend`) | ✅ | ✅ | Full backend. Uses LISTEN\u002FNOTIFY for notifications. |\n| **Redis** (`RedisBackend`) | ✅ | ✅ | Full backend. Uses Pub\u002FSub for notifications. |\n| **InMemoryNotificationChannel** | ❌ | ✅ | Notifications only. For single-process\u002Ftesting. |\n| **RedisNotificationChannel** | ❌ | ✅ | Notifications only. For multi-process with MongoDB storage. |\n\n#### Mixing Storage and Notification Backends\n\nYou can combine MongoDB storage with a separate notification channel for real-time job processing:\n\n```js\nimport { Agenda } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\nimport { RedisBackend } from '@agendajs\u002Fredis-backend';\n\n\u002F\u002F MongoDB for storage + Redis for real-time notifications\nconst redisBackend = new RedisBackend({ connectionString: 'redis:\u002F\u002Flocalhost:6379' });\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  notificationChannel: redisBackend.notificationChannel\n});\n\n\u002F\u002F Or use PostgreSQL notifications with MongoDB storage\nimport { PostgresBackend } from '@agendajs\u002Fpostgres-backend';\nconst pgBackend = new PostgresBackend({ connectionString: 'postgres:\u002F\u002F...' });\nconst agenda = new Agenda({\n  backend: new MongoBackend({ mongo: db }),\n  notificationChannel: pgBackend.notificationChannel\n});\n```\n\nThis is useful when you want to keep MongoDB for job storage (proven durability, flexible queries) but need faster real-time notifications across multiple processes.\n\nSee [Backend Configuration](#backend-configuration) for setup details.\n\n### Spawning \u002F forking processes\n\nUltimately Agenda can work from a single job queue across multiple machines, node processes, or forks. If you are interested in having more than one worker, [Bars3s](http:\u002F\u002Fgithub.com\u002Fbars3s) has written up a fantastic example of how one might do it:\n\n```js\nconst cluster = require('cluster');\nconst os = require('os');\n\nconst httpServer = require('.\u002Fapp\u002Fhttp-server');\nconst jobWorker = require('.\u002Fapp\u002Fjob-worker');\n\nconst jobWorkers = [];\nconst webWorkers = [];\n\nif (cluster.isMaster) {\n\tconst cpuCount = os.cpus().length;\n\t\u002F\u002F Create a worker for each CPU\n\tfor (let i = 0; i \u003C cpuCount; i += 1) {\n\t\taddJobWorker();\n\t\taddWebWorker();\n\t}\n\n\tcluster.on('exit', (worker, code, signal) => {\n\t\tif (jobWorkers.indexOf(worker.id) !== -1) {\n\t\t\tconsole.log(\n\t\t\t\t`job worker ${worker.process.pid} exited (signal: ${signal}). Trying to respawn...`\n\t\t\t);\n\t\t\tremoveJobWorker(worker.id);\n\t\t\taddJobWorker();\n\t\t}\n\n\t\tif (webWorkers.indexOf(worker.id) !== -1) {\n\t\t\tconsole.log(\n\t\t\t\t`http worker ${worker.process.pid} exited (signal: ${signal}). Trying to respawn...`\n\t\t\t);\n\t\t\tremoveWebWorker(worker.id);\n\t\t\taddWebWorker();\n\t\t}\n\t});\n} else {\n\tif (process.env.web) {\n\t\tconsole.log(`start http server: ${cluster.worker.id}`);\n\t\t\u002F\u002F Initialize the http server here\n\t\thttpServer.start();\n\t}\n\n\tif (process.env.job) {\n\t\tconsole.log(`start job server: ${cluster.worker.id}`);\n\t\t\u002F\u002F Initialize the Agenda here\n\t\tjobWorker.start();\n\t}\n}\n\nfunction addWebWorker() {\n\twebWorkers.push(cluster.fork({ web: 1 }).id);\n}\n\nfunction addJobWorker() {\n\tjobWorkers.push(cluster.fork({ job: 1 }).id);\n}\n\nfunction removeWebWorker(id) {\n\twebWorkers.splice(webWorkers.indexOf(id), 1);\n}\n\nfunction removeJobWorker(id) {\n\tjobWorkers.splice(jobWorkers.indexOf(id), 1);\n}\n```\n\n### Recovering lost database connections\n\nAgenda emits an [error event](#agenda-events) when no database connection is available on each [process tick](#processeveryinterval), allowing you to handle connection issues without restarting the application.\n\nIf you are using a notification channel, it includes built-in reconnection logic with configurable retry attempts and exponential backoff. See the [Notification Channel](#notification-channel-real-time-job-processing) section for details.\n\n# Example Project Structure\n\nAgenda will only process jobs that it has definitions for. This allows you to\nselectively choose which jobs a given agenda will process.\n\nConsider the following project structure, which allows us to share models with\nthe rest of our code base, and specify which jobs a worker processes, if any at\nall.\n\n```\n- server.js\n- worker.js\nlib\u002F\n  - agenda.js\n  controllers\u002F\n    - user-controller.js\n  jobs\u002F\n    - email.js\n    - video-processing.js\n    - image-processing.js\n   models\u002F\n     - user-model.js\n     - blog-post.model.js\n```\n\nSample job processor (eg. `jobs\u002Femail.js`)\n\n```js\nlet email = require('some-email-lib'),\n\tUser = require('..\u002Fmodels\u002Fuser-model.js');\n\nmodule.exports = function (agenda) {\n\tagenda.define('registration email', async job => {\n\t\tconst user = await User.get(job.attrs.data.userId);\n\t\tawait email(user.email(), 'Thanks for registering', 'Thanks for registering ' + user.name());\n\t});\n\n\tagenda.define('reset password', async job => {\n\t\t\u002F\u002F Etc\n\t});\n\n\t\u002F\u002F More email related jobs\n};\n```\n\nlib\u002Fagenda.js\n\n```js\nimport { Agenda } from 'agenda';\nimport { MongoBackend } from '@agendajs\u002Fmongo-backend';\n\nconst agenda = new Agenda({\n\tbackend: new MongoBackend({\n\t\taddress: 'mongodb:\u002F\u002Flocalhost:27017\u002Fagenda-test',\n\t\tcollection: 'agendaJobs'\n\t})\n});\n\nconst jobTypes = process.env.JOB_TYPES ? process.env.JOB_TYPES.split(',') : [];\n\njobTypes.forEach(type => {\n\trequire('.\u002Fjobs\u002F' + type)(agenda);\n});\n\nif (jobTypes.length) {\n\tagenda.start(); \u002F\u002F Returns a promise, which should be handled appropriately\n}\n\nmodule.exports = agenda;\n```\n\nlib\u002Fcontrollers\u002Fuser-controller.js\n\n```js\nlet app = express(),\n\tUser = require('..\u002Fmodels\u002Fuser-model'),\n\tagenda = require('..\u002Fworker.js');\n\napp.post('\u002Fusers', (req, res, next) => {\n\tconst user = new User(req.body);\n\tuser.save(err => {\n\t\tif (err) {\n\t\t\treturn next(err);\n\t\t}\n\t\tagenda.now('registration email', { userId: user.primary() });\n\t\tres.send(201, user.toJson());\n\t});\n});\n```\n\nworker.js\n\n```js\nrequire('.\u002Flib\u002Fagenda.js');\n```\n\nNow you can do the following in your project:\n\n```bash\nnode server.js\n```\n\nFire up an instance with no `JOB_TYPES`, giving you the ability to process jobs,\nbut not wasting resources processing jobs.\n\n```bash\nJOB_TYPES=email node server.js\n```\n\nAllow your http server to process email jobs.\n\n```bash\nJOB_TYPES=email node worker.js\n```\n\nFire up an instance that processes email jobs.\n\n```bash\nJOB_TYPES=video-processing,image-processing node worker.js\n```\n\nFire up an insta","Agenda 是一个轻量级的 Node.js 任务调度库。它支持通过 cron 表达式或人类可读语法进行任务调度，提供优先级、并发控制以及任务结果持久化等功能。Agenda 基于 MongoDB 实现了后端存储，并且支持自定义后端驱动程序，使得开发者可以根据需要选择合适的存储方案。此外，Agenda 还引入了实时通知机制，允许使用 Redis、PostgreSQL LISTEN\u002FNOTIFY 或者 MongoDB Change Streams 等技术实现即时的任务处理反馈。该项目适用于需要定期执行特定任务的应用场景，如定时数据备份、邮件发送等自动化操作。","2026-06-11 03:26:53","top_topic"]