A schedule is a named cron rule attached to a queue. When a tick fires, Rotor enqueues a job to that queue with the payload you specified — your callback handler receives it exactly like any other job. You don't run a cron process; Rotor does.
Use schedules when you need to:
- Kick off a nightly enrichment or sync at a specific time in a specific timezone
- Send a weekly digest every Monday morning in each customer's local time
- Trigger a daily report at 9 AM New York time regardless of daylight saving
Creating a schedule
CLI
rotor schedules create \
--queue enrichment \
--name nightly-enrichment \
--cron "0 2 * * *" \
--timezone "America/New_York" \
--job-data '{"source":"clearbit"}'SDK
import { Rotor } from "@rotorsh/sdk";
const rotor = new Rotor({ apiKey: process.env.ROTOR_API_KEY! });
const schedule = await rotor.schedules.create({
queue_name: "enrichment",
name: "nightly-enrichment",
cron: "0 2 * * *",
timezone: "America/New_York",
job_data: { source: "clearbit" },
enabled: true,
});
console.log(schedule.external_id); // sched_<32hex> — save thisThe external_id (sched_<32hex>) is your handle for all subsequent operations. Save it.
The timezone requirement
timezone is required. Rotor rejects schedule creation without it.
The reason matters: cron expressions like 0 9 * * 1-5 (9 AM weekdays) are meaningless without a timezone. If you use UTC when your team is in New York, the job fires at 9 AM UTC — which is 5 AM ET in winter and 4 AM ET when daylight saving ends. With a timezone, Rotor recalculates the next-fire timestamp whenever clocks change so the job always fires at the wall-clock time you intended.
Pass a valid IANA timezone string (e.g., "America/New_York", "Europe/London", "Asia/Tokyo"). Offsets like "+05:30" are not accepted — they don't account for DST transitions.
external_id vs internal id
Every schedule has two identifiers:
| Field | Format | Use |
|---|---|---|
external_id | sched_<32hex> | Use in all SDK and CLI calls |
id | ws_abc:name | BullMQ internal key — ignore this |
Always use external_id. The internal id is an implementation detail that may change.
Updating a schedule
You can update the cron expression, timezone, payload, or enabled state independently — any combination of fields.
// Change the cron — now fires at 3 AM instead of 2 AM
await rotor.schedules.update("sched_abc123...", {
cron: "0 3 * * *",
});
// Change the payload
await rotor.schedules.update("sched_abc123...", {
job_data: { source: "apollo", limit: 500 },
});
// Change timezone — e.g., follow a customer moving regions
await rotor.schedules.update("sched_abc123...", {
timezone: "Europe/London",
});Pausing and resuming
Set enabled: false to pause a schedule. No ticks fire while paused. Set enabled: true to resume — the next tick fires at the next cron-calculated time after resume.
// Pause
await rotor.schedules.update("sched_abc123...", { enabled: false });
// Resume
await rotor.schedules.update("sched_abc123...", { enabled: true });Pausing a schedule does not cancel jobs that are already enqueued and waiting in the queue. It only prevents future ticks from creating new jobs.
Deleting a schedule
await rotor.schedules.delete("sched_abc123...");CLI:
rotor schedules delete sched_abc123...Deletion is permanent. The schedule stops firing immediately. Run history is retained for your plan's retention window.
Fire now
fireNow enqueues a job immediately — the same job the schedule would enqueue at its next tick. Use it to test a new schedule without waiting for the first cron tick, or to manually backfill a missed run.
const { jobId } = await rotor.schedules.fireNow("sched_abc123...");
console.log(jobId); // job_<id> — track this in rotor.jobs.get()CLI:
rotor schedules fire-now sched_abc123...Manual fireNow calls appear in the run history alongside cron-driven ticks — both are ScheduleRun records.
Run history
Rotor records every tick (cron-driven and manual) as a ScheduleRun.
const { runs } = await rotor.schedules.runs("sched_abc123...").list({ limit: 20 });
for (const run of runs) {
console.log(run.id); // sr_<32hex>
console.log(run.scheduled_for); // ISO timestamp — when the tick was due
console.log(run.fired_at); // ISO timestamp — when Rotor actually enqueued
console.log(run.job_id); // job_<id> — the resulting job
console.log(run.callback_status); // pending | 2xx | 4xx | 5xx | timeout | null
console.log(run.error); // error message if status is 4xx/5xx/timeout
}callback_status values
| Value | Meaning |
|---|---|
pending | Job enqueued, callback not yet called |
2xx | Your handler returned a success response |
4xx | Your handler returned a client error (check error field) |
5xx | Your handler returned a server error — will retry per queue config |
timeout | Your handler did not respond within the deadline |
null | Status not yet recorded |
CLI:
rotor schedules get sched_abc123...Listing schedules
rotor schedules listconst schedules = await rotor.schedules.list();Common cron patterns
| Expression | Meaning | Typical timezone |
|---|---|---|
0 9 * * 1-5 | 9 AM weekdays | America/New_York |
0 2 * * * | 2 AM daily | UTC |
0 8 * * 1 | 8 AM every Monday | America/Chicago |
0 0 1 * * | Midnight on the 1st of each month | UTC |
*/15 * * * * | Every 15 minutes | UTC |
0 18 * * 5 | 6 PM every Friday | Europe/London |
Use crontab.guru to verify expressions before creating a schedule. Paste the expression and confirm the human-readable description matches your intent.