Skip to main content
A scheduler provider decides when to fire. The default polls in-process; swap it for systemd, a cloud scheduler webhook, or a cron trigger without changing what fires. A provider never decides what fires — it just calls on_due whenever a tick should occur, and the shared runner and store claim and fire due jobs.

Which Provider?

Pick in-process when one host runs all the time; pick an external provider when firing should come from elsewhere.

Quick Start

1

Default — in-process poll (nothing to configure)

2

External / serverless — event-driven, no always-on thread


How It Works

The provider owns the trigger; the runner and store own the firing. on_due is the seam: the provider calls it whenever its trigger fires, and everything downstream — claiming due jobs and firing them — happens in the runner and store.

Provider Options

Two ways to drive firing — one always-on, one event-driven. Any object with start(on_due, store=...) and stop() satisfies SchedulerProviderProtocol.

Sharing the store across providers

When a webhook, systemd timer, or K8s CronJob triggers engine.fire_due() and the gateway or wrapper is also polling, all of them should read the same store so a job authored anywhere is claimed once and only once.
For deployments that need a different backend at process start, override once — every downstream consumer (agent tools, gateway tick, host bridge) picks it up.
All three consumers resolve their store the same way, so a job written on any surface fires on every tick loop.

User Interaction Flow

A reminder set through the agent still fires even when a cloud scheduler drives the tick.

Configuration Options

Providers are protocol-only — see the auto-generated SDK reference for full signatures.

Scheduler Protocols

SchedulerProviderProtocol, ScheduleStoreProtocol, JobConditionProtocol, GateResult

Schedule Tools

Agent-callable schedule_add / schedule_list / schedule_remove

Common Patterns

Cloud webhook

Cloud Scheduler, EventBridge, or a GitHub Actions cron posts to an HTTP handler that calls fire_due().

systemd / launchd timer

A timer unit runs a short command on schedule; the command fires one tick and exits.

Kubernetes CronJob

A short-lived pod runs fire_due() and exits — no long-running scheduler pod.

Best Practices

start() on a running loop is a no-op by design. Treat one loop as one provider — don’t try to reconfigure it mid-run.
In webhook, cron, or systemd handlers call engine.fire_due() and never call start() — you get one tick’s work with no daemon thread.
Both bundled stores (ConfigYamlScheduleStore — the default — and FileScheduleStore) implement atomic claim_due, so a due job fires at most once across processes and hosts out of the box. fire_due() also runs its claim + fire step under a per-instance lock, guarding against double-fires within a single process.OS advisory locks only hold on filesystems that honour them — NFS does not reliably, so shared-NFS deployments can still double-fire.
Both bundled stores already implement claim_due, so cross-host at-most-once delivery works by default. If you bring a custom store, implement claim_due on it to keep the same guarantee when firing from multiple hosts.
The unified-store fix in PraisonAI #3266 relies on writer and reader landing on the same backend. If you construct ConfigYamlScheduleStore() in one place and FileScheduleStore() in another, jobs authored on one side silently miss the ticker on the other. Resolve every consumer through get_default_store().

Background Tasks — ScheduleLoop

The in-process poll loop and combined recipes

Schedule Tools

Let agents create schedules via tool calls

Scheduled Run Policy

Safety gate for unattended scheduled runs

Scheduler Pre-Run Gate

Skip ticks cheaply before spending tokens