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 this

The 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.

Warning

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:

FieldFormatUse
external_idsched_<32hex>Use in all SDK and CLI calls
idws_abc:nameBullMQ 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 });
Note

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

ValueMeaning
pendingJob enqueued, callback not yet called
2xxYour handler returned a success response
4xxYour handler returned a client error (check error field)
5xxYour handler returned a server error — will retry per queue config
timeoutYour handler did not respond within the deadline
nullStatus not yet recorded

CLI:

rotor schedules get sched_abc123...

Listing schedules

rotor schedules list
const schedules = await rotor.schedules.list();

Common cron patterns

ExpressionMeaningTypical timezone
0 9 * * 1-59 AM weekdaysAmerica/New_York
0 2 * * *2 AM dailyUTC
0 8 * * 18 AM every MondayAmerica/Chicago
0 0 1 * *Midnight on the 1st of each monthUTC
*/15 * * * *Every 15 minutesUTC
0 18 * * 56 PM every FridayEurope/London
Tip

Use crontab.guru to verify expressions before creating a schedule. Paste the expression and confirm the human-readable description matches your intent.

Next steps