Scheduler
Stem Beat coordinates periodic work across your cluster. Explore the scheduler capabilities, storage backends, and operational tooling.
What Beat does
Beat reads schedule definitions from a schedule store, evaluates when each entry is due, and enqueues the corresponding task. Use it for cron-style jobs, interval tasks, solar events, or one-off clocked runs.
Schedule spec types
Stem ships concrete spec types you can store or generate:
- Interval (
IntervalScheduleSpec) — run every N seconds/minutes/hours. - Cron (
CronScheduleSpec) — standard cron expressions. - Solar (
SolarScheduleSpec) — sunrise/sunset-based schedules. - Clocked (
ClockedScheduleSpec) — single run at a specific time.
Beat in production
Beat is a separate process from workers. It only enqueues tasks; workers still execute them. That separation means you can scale Beat (and its schedule store) independently from worker fleets.
HA and lock stores
To run Beat in high availability mode, multiple Beat instances can share the same schedule store and a lock store. The lock store ensures only one scheduler emits a given schedule entry at a time. Redis and Postgres stores support this pattern out of the box.
Schedule stores
Beat persists schedule entries so restarts do not lose state. For production,
use a shared schedule store (Redis/Postgres) and a lock store to coordinate HA
instances. The CLI schedule commands use STEM_SCHEDULE_STORE_URL when set;
otherwise they operate on local schedule files.
CLI entrypoints
Common scheduler CLI commands:
stem schedule apply— load schedule entries from JSON/YAML into the store.stem schedule list— inspect entries in the store.stem schedule dry-run— preview due times before rollout.stem observe schedules— inspect schedule drift and dispatch status.
Scheduler snippets
Future<void> addIntervalSchedule(ScheduleStore store) async {
await store.upsert(
ScheduleEntry(
id: 'interval-demo',
taskName: 'demo.interval',
queue: 'default',
spec: IntervalScheduleSpec(
every: const Duration(minutes: 10),
),
jitter: const Duration(minutes: 1),
),
);
}
Future<void> addCronSchedule(ScheduleStore store) async {
await store.upsert(
ScheduleEntry(
id: 'cron-demo',
taskName: 'demo.cron',
queue: 'default',
spec: CronScheduleSpec(expression: '0 12 * * MON'),
),
);
}
Future<void> addSolarSchedule(ScheduleStore store) async {
await store.upsert(
ScheduleEntry(
id: 'solar-demo',
taskName: 'demo.solar',
queue: 'default',
spec: SolarScheduleSpec(
event: 'sunrise',
latitude: 51.5072,
longitude: -0.1276,
offset: const Duration(minutes: 30),
),
),
);
}
Future<void> addClockedSchedule(ScheduleStore store) async {
final runAt = DateTime.now().add(const Duration(hours: 6));
await store.upsert(
ScheduleEntry(
id: 'clocked-demo',
taskName: 'demo.clocked',
queue: 'default',
spec: ClockedScheduleSpec(runAt: runAt),
),
);
}
void registerBeatSignals() {
StemSignals.scheduleEntryDue.connect((payload, _) {
print('[due] ${payload.entry.id} @ ${payload.tickAt}');
});
StemSignals.scheduleEntryDispatched.connect((payload, _) {
print('[dispatched] drift=${payload.drift.inMilliseconds}ms');
});
StemSignals.scheduleEntryFailed.connect((payload, _) {
print('[failed] ${payload.entry.id}: ${payload.error}');
});
}
Beat itself runs as a Dart process; see the Beat guide for entrypoints.
- Beat Scheduler Guide – Configure Beat, load schedules, and run it with in-memory, Redis, or Postgres stores.
- Example:
example/scheduler_observabilityshows drift metrics, schedule signals, and CLI inspection.
Looking for locking and storage details? See the Postgres and Redis sections in Broker Overview.