Skip to content

Scheduling

Monque supports recurring job scheduling using standard 5-field cron expressions.

Method / TypeDescription
schedule()Create a recurring cron-based job
ScheduleOptionsOptions for schedule()
Job.repeatIntervalCron expression for recurring jobs
InvalidCronErrorThrown for invalid cron expressions

Use schedule() to create recurring jobs:

import { Monque } from '@monque/core';

// Run every hour at minute 0
await monque.schedule('0 * * * *', 'hourly-report', {
  type: 'analytics'
});
schedule<T>(
  cronExpression: string,  // 5-field cron expression
  name: string,            // Job type name
  data: T,                 // Job payload
  options?: ScheduleOptions
): Promise<PersistedJob<T>>
interface ScheduleOptions {
  uniqueKey?: string;  // Prevent duplicate scheduled jobs
}

Monque uses the standard 5-field cron format:

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
│ │ │ │ │
* * * * *
CharacterDescriptionExample
*Any value* * * * * (every minute)
,List separator0,30 * * * * (0 and 30 minutes)
-Range0-15 * * * * (minutes 0-15)
/Step values*/5 * * * * (every 5 minutes)

Monque supports cron aliases provided by the cron-parser library for common schedules:

AliasDescriptionEquivalent
@yearly (or @annually)Run once a year0 0 1 1 *
@monthlyRun once a month0 0 1 * *
@weeklyRun once a week0 0 * * 0
@daily (or @midnight)Run once a day0 0 * * *
@hourlyRun once an hour0 * * * *
// Run daily at midnight using alias
await monque.schedule('@daily', 'daily-cleanup', {});
// Every minute
await monque.schedule('* * * * *', 'heartbeat-check', {});

// Every 5 minutes
await monque.schedule('*/5 * * * *', 'sync-cache', {});

// Every hour at minute 0
await monque.schedule('0 * * * *', 'hourly-cleanup', {});

// Every 6 hours
await monque.schedule('0 */6 * * *', 'refresh-tokens', {});
// Every day at midnight
await monque.schedule('0 0 * * *', 'daily-report', {
  reportType: 'sales'
});

// Every day at 9 AM
await monque.schedule('0 9 * * *', 'morning-briefing', {
  channels: ['email', 'slack']
});

// Twice daily at 6 AM and 6 PM
await monque.schedule('0 6,18 * * *', 'inventory-sync', {});
// Every Monday at 9 AM
await monque.schedule('0 9 * * 1', 'weekly-summary', {
  reportType: 'performance'
});

// Weekdays at 8 AM
await monque.schedule('0 8 * * 1-5', 'workday-startup', {});

// Every Sunday at midnight
await monque.schedule('0 0 * * 0', 'weekly-cleanup', {});
// First day of month at midnight
await monque.schedule('0 0 1 * *', 'monthly-billing', {});

// 15th of every month at noon
await monque.schedule('0 12 15 * *', 'mid-month-review', {});

// Last day approach: run on 28th (safe for all months)
await monque.schedule('0 0 28 * *', 'monthly-summary', {});

When you call schedule():

  1. Cron expression is validated
  2. Next run time is calculated from current time
  3. Job is created with repeatInterval set to the cron expression
const job = await monque.schedule('0 * * * *', 'hourly-task', { foo: 'bar' });

console.log(job.repeatInterval); // '0 * * * *'
console.log(job.nextRunAt);      // Next hour, minute 0

When a recurring job completes successfully:

  1. Status changes to completed
  2. Monque calculates next run time from current time
  3. Job is reset to pending with new nextRunAt

For '0 * * * *' (hourly at minute 0):

10:00 - Job created, nextRunAt = 11:00
11:00 - Job runs, completes at 11:02
11:02 - Rescheduled, nextRunAt = 12:00
12:00 - Job runs, fails
12:00 - Retry scheduled, nextRunAt = 12:00 + backoff
12:02 - Retry succeeds
12:02 - Rescheduled, nextRunAt = 13:00

Use uniqueKey to ensure only one scheduled instance exists:

// Only one hourly report job will exist
await monque.schedule('0 * * * *', 'hourly-report', {}, {
  uniqueKey: 'hourly-report-singleton'
});

// Safe to call multiple times (during app startup, etc.)
await monque.schedule('0 * * * *', 'hourly-report', {}, {
  uniqueKey: 'hourly-report-singleton'
});
// Second call returns existing job, no duplicate created

Monque throws InvalidCronError for invalid expressions:

import { InvalidCronError, Monque } from '@monque/core';

try {
  await monque.schedule('invalid cron', 'job', {});
} catch (error) {
  if (error instanceof InvalidCronError) {
    console.log('Invalid expression:', error.expression);
    console.log('Error:', error.message);
    // Error includes: position of error, valid format example
  }
}

Monque provides detailed error messages for invalid expressions:

await monque.schedule('60 * * * *', 'job', {});
// InvalidCronError: Invalid cron expression "60 * * * *": 
// minute (field 1) value 60 is out of range (0-59)
// If your server is in UTC, this runs at 9 AM UTC
// If your server is in EST, this runs at 9 AM EST
await monque.schedule('0 9 * * *', 'daily-task', {
  note: 'Check your server timezone for consistent scheduling'
});
import { JobStatus } from '@monque/core';

const recurringJobs = await monque.getJobs({
  status: [JobStatus.PENDING, JobStatus.PROCESSING]
});

const scheduledJobs = recurringJobs.filter(job => job.repeatInterval);

for (const job of scheduledJobs) {
  console.log(`${job.name}: ${job.repeatInterval} - Next: ${job.nextRunAt}`);
}

To stop a recurring job, delete it from the database:

const collection = db.collection('monque_jobs');

// Delete by uniqueKey
await collection.deleteOne({ uniqueKey: 'hourly-report-singleton' });

// Or by name (careful: deletes all jobs with this name)
await collection.deleteMany({ name: 'hourly-report' });
// ✅ Good
await monque.schedule('0 0 * * *', 'daily-user-activity-digest', {});
await monque.schedule('*/15 * * * *', 'sync-inventory-with-warehouse', {});

// ❌ Bad
await monque.schedule('0 0 * * *', 'job1', {});
await monque.schedule('*/15 * * * *', 'sync', {});
await monque.schedule('0 9 * * 1', 'weekly-report', {
  reportType: 'sales',
  recipients: ['team@example.com'],
  includePreviousWeek: true
});

For long-running scheduled jobs, use uniqueKey to prevent overlap:

// If job takes longer than 1 hour, next run waits
await monque.schedule('0 * * * *', 'long-running-sync', {}, {
  uniqueKey: 'long-sync-singleton'
});
monque.on('job:complete', ({ job }) => {
  if (job.repeatInterval) {
    console.log(`Recurring job ${job.name} completed. Next run: ${job.nextRunAt}`);
  }
});