Built-in — no extra dependencies required. Schedule tools are included in the core
praisonaiagents package.pre_run so expensive model turns only happen when there’s real work to do. No changes to the Agent class are needed.
Quick Start
1
Simple Usage
2
With Configuration
schedule_add with the appropriate schedule expression, and the job will be persisted to disk.
Available Tools
schedule_add
Add a new scheduled job.
Returns: Confirmation string with the job id.
schedule_list
List all scheduled jobs. Takes no parameters. Returns: Formatted string listing every job with id, name, schedule, status, and message.schedule_remove
Remove a scheduled job by name.
Returns: Confirmation or not-found message.
schedule_pause
Pause a schedule by name. Setsenabled=False so every due-check skips the job without deleting it — the counterpart to schedule_resume.
Returns: Confirmation or not-found message.
schedule_resume
Resume a paused schedule by name. Setsenabled=True so the job fires again on its next due tick.
Returns: Confirmation or not-found message.
schedule_update
Update a schedule’s cadence and/or message. Only the fields you pass change; empty values leave the current value untouched. Changingschedule clears last_run_at so the new cadence runs fresh.
Returns: Confirmation or not-found / error message.
The CLI exposes the same surface — see
praisonai schedule pause / resume / update.
Schedule Expressions
Pick the format that matches how the job should recur.A naive
at: timestamp (no Z, no +HH:MM) is a wall-clock reading. As of PraisonAI #4732 the parser stamps it with the resolved zone at parse time, so the stored value names an unambiguous instant. See Timezones for at: and cron:.Examples
Recurring Schedule
One-Shot Reminder
Fire-and-forget vs. continuable
A delivered brief is continuable by default — a reply in the same chat resumes the job’s conversation. For pure alerts, setcontinuable=False so a reply stays a fresh turn.
List and Manage
One-Shot Job
Using String Tool Names
Timezones for at: and cron:
Which wall-clock time a schedule uses depends on the timezone it resolves to.
For a naive at: ISO timestamp (no Z, no +HH:MM) and the natural-language clock forms (at 9am, at 17:00), the parser resolves the zone in order:
- The
tzargument passed to the schedule (per-schedule). - The instance default (the
scheduler.timezonekey inconfig.yaml). - The
PRAISONAI_SCHEDULE_TIMEZONEenvironment variable (process-wide default). - The machine’s local zone.
at: string that already carries an offset (+05:30, Z, +00:00) is used verbatim — the offset in the string always wins.
cron: expressions follow the same 1–3 precedence but fall back to UTC at step 4, unchanged from previous releases. Only the naive-at: path defaults to the local zone.
The stored value is aware
As of PraisonAI #4732,parse_schedule returns a Schedule whose at is already an aware ISO string. A job authored on one machine and later polled by a runner in a different zone fires at the same wall-clock time the person originally typed — the guess is_due() used to make at fire time is gone.
DST is handled per target date
localize_wall_clock attaches the offset in force on the target date, not the offset in force right now. In Europe/London, at:2026-07-01T09:00:00 stamps +01:00 (BST) even if you parse it in January, and at:2026-12-01T09:00:00 stamps +00:00 (GMT) even if you parse it in July. You do not need to hand-compute offsets in the ISO string.
PRAISONAI_SCHEDULE_TIMEZONE fails loudly on bad names
An unknown IANA zone in PRAISONAI_SCHEDULE_TIMEZONE (or in the tz= argument) now raises ValueError from parse_schedule when it hits an at: or clock form, instead of being accepted silently and never firing. A Schedule(kind="at", at=<naive iso>) built directly hits the same check when is_due() reads it.
Legacy stored jobs
Jobs stored before #4705 were read as UTC. Since #4705 they are read in the local zone; #4732 does not re-move them, but the DST correction means a legacy naive
at on the other DST half of the year is now read at the correct wall-clock time instead of an hour off. Jobs stored after #4732 carry the offset in the ISO string and are portable across machines. in N minutes was already stored as an aware UTC instant and is unchanged. Hand-written config.yaml at: strings that omit an offset are still supported — they fall through the same localize_wall_clock path in is_due(), using the schedule’s tz, else the store default, else PRAISONAI_SCHEDULE_TIMEZONE, else the runner’s local zone.Agent self-management flow
The agent creates, pauses, updates, resumes, and inspects a schedule entirely through tool calls in a single conversation.schedule_pause, schedule_resume, and schedule_update use the same _get_store() singleton as schedule_add / schedule_list / schedule_remove, so a job authored by any tool is fully manageable by every tool.Storage
Jobs are persisted to~/.praisonai/config.yaml under the schedules key by default via ConfigYamlScheduleStore. The store is:
- Thread-safe for multi-agent scenarios
- Atomic writes (tmp + rename) to prevent corruption
- Auto-created on first use
- Auto-migrates legacy
jobs.jsondata on first load
This exact store instance is shared by the gateway scheduler tick and the wrapper’s schedules bridge. Jobs authored by an agent tool are polled by every runtime — you no longer see “scheduled ✓” silently drop.
Shared Default Store
Every runtime shares one process-wide store viaget_default_store() so a job authored by an agent is claimed by the gateway ticker, not silently dropped.
praisonaiagents.tools.schedule_tools.set_store(my_store) continues to work and now also repoints the canonical default under the hood.
Custom Store (ScheduleStoreProtocol)
Swap the default file store for any backend that implementsScheduleStoreProtocol:
schedule_add/list/remove calls use your store:
set_store() now also repoints the process-wide scheduler.get_default_store() so the gateway tick and host bridge pick up the same backend. This is best-effort and logs a warning if the repoint fails; the tool store you passed is always authoritative for the agent tools.
PraisonAIUI and BotOS use the same
config.yaml store. You can also call set_store() to inject any custom backend.Custom Provider (SchedulerProviderProtocol)
Swap the default in-process poll thread for any backend that decides when to fire:Schedule Runner
TheScheduleRunner checks which jobs are due for execution:
ConfigYamlScheduleStore() directly still works — the process-wide get_default_store() returns the same class by default and is the recommended way to share one instance with the gateway tick and host bridge.
Hook Events
Schedule lifecycle events are available via the hook system:Execution History
Every scheduled job execution is logged as aRunRecord for auditing:
Executing Scheduled Jobs
Schedule tools create and persist jobs, but to actually execute them when they’re due, useScheduleLoop:
See Background Tasks — ScheduleLoop for the full API and combined examples with
BackgroundRunner.Pre-Run Condition Gate
Gate a scheduled tick on a cheap shell check so no model tokens are spent when there’s nothing to do.1
Add pre_run to a schedule in bot.yaml
2
Every tick, PraisonAI evaluates pre_run before spending tokens
pre_run is a cost gate (decides whether to run). It is not a safety gate (RunPolicy, which decides what a run may do). Use both when you need both.Real-World Examples
Only triage when new issues exist:Custom Condition Gate
Any object implementingJobConditionProtocol can replace the default shell gate — a Python callable, an MCP probe, a database check.
condition_resolver=False to disable gating entirely. The default resolver automatically activates ShellConditionGate for any job that has a pre_run value.
BotOS Integration
When using BotOS (multi-platform bot orchestrator), scheduled jobs execute automatically — noScheduleLoop needed. BotOS runs its own 30-second schedule tick alongside all bots:
- Agents create jobs via
schedule_addduring conversations - BotOS detects due jobs every 30 seconds
- The originating agent processes the job message
- Results are delivered back to the originating platform (Telegram, Discord, etc.)
Architecture
Schedule tools follow PraisonAI’s core principles:- Agent-centric — tools, not Agent parameters
- Lazy-loaded — zero import cost until used
- Protocol-driven —
ScheduleStoreProtocolmakes stores swappable - No Agent bloat — the
Agentclass is unchanged - Thread-safe — safe for multi-agent workflows
- Pluggable —
set_store()lets any backend replace the default file store
See Also
Schedule CLI
CLI equivalents: add, list, pause, resume, update, remove
Background Tasks
Sync wrappers, ScheduleLoop, and combined recipes
Scheduler CLI
24/7 autonomous agent scheduling via CLI
Best Practices
Use cron expressions for precise schedules
Use cron expressions for precise schedules
Cron expressions give exact control over scheduling - prefer them for production use.
Log scheduled job execution
Log scheduled job execution
Add logging to scheduled agent tasks so you can verify they ran and diagnose failures.
Test with short intervals first
Test with short intervals first
Use 1-minute intervals during testing, then switch to production schedules before deployment.
Handle job failures gracefully
Handle job failures gracefully
Scheduled jobs should catch exceptions and report errors rather than silently failing.
Use pre_run to avoid paying for empty ticks
Use pre_run to avoid paying for empty ticks
If a schedule only has work when some external state changes (new emails, new PRs, a queue with pending rows), put the cheap check in
pre_run. Model tokens are spent only for ticks that actually have work to do.Related
Custom Tools
Build your own agent tools
Tools Overview
Browse PraisonAI tool documentation

