Quick Start
With Direct Prompt (No YAML needed)
With agents.yaml
Installation
Deliver Scheduled Results to a Chat Channel
Route each successful scheduled run to Telegram, Discord, Slack, or WhatsApp — no gateway required. Set adeliver token from Python, YAML, or the CLI:
- Python
- YAML
- CLI
Token Grammar
Thedeliver token uses DeliveryTarget.parse. Whitespace is stripped and origin/all are case-insensitive.
Optional Dependency
Delivery goes through thepraisonai-bot package.
A CLI
--deliver token overrides any target resolved from YAML or a recipe.Delivery outcomes
Every scheduled run now resolves to one of four typed outcomes — a failed delivery is reported throughon_failure instead of being a silent success (PraisonAI PR #4476). This applies to both the scheduled loop and the one-time execute_once() path.
See Scheduler Delivery → Delivery outcomes for the full flow, stats fields, and the
MESSAGE_UNDELIVERED hook.
Running schedule tick without a live gateway
praisonai schedule tick runs each due job once and delivers its deliver: target — you can schedule it straight from OS cron / CI without keeping any long-running gateway process alive. When no live gateway is wired, delivery falls back to a stateless, token-authenticated standalone sender for Telegram / Slack / Discord.
The minimum for a Telegram cron entry is two env vars — a bot token plus a default channel:
HOME_CHANNEL and target the chat explicitly on the job (deliver: telegram:<chat_id>) — the token env is still required.
See Out-of-process delivery for the env-var table, chat-id resolution order, and per-platform failure modes.
origin is resolved from the persisted ScheduleJob.origin — safe on the lightweight scheduler path (it delivers back to the same channel/thread the request came in on). Only all still requires the full BotOS gateway (it enumerates every registered bot); setting all on the lightweight path logs a warning and skips delivery.Delivery outcomes
Every scheduled run resolves to one of four typed outcomes, and a failed delivery is no longer a silent success — an undelivered result fireson_failure("scheduled result could not be delivered") and bumps undelivered_deliveries. This applies to both the scheduled loop and the one-time execute_once().
See Scheduler Delivery → Delivery outcomes for the full flow, stats, and
MESSAGE_UNDELIVERED hook.
Async Delivery
AsyncAgentScheduler accepts the same deliver= parameter and dispatches delivery through asyncio.to_thread because the shared helper uses the sync bridge — it never raises and never blocks the event loop.
Delivery Best Practices
Prefer platform:channel_id for reliable routing
Prefer platform:channel_id for reliable routing
A bare
platform token depends on the router resolving the platform’s home channel. Use telegram:123456 to target an explicit chat and avoid ambiguity.Keep the token in YAML for reproducibility
Keep the token in YAML for reproducibility
Store
schedule.deliver in YAML so scheduled jobs are reproducible. Override with -d only for one-off runs.origin resolves on the lightweight path; all needs the gateway
origin resolves on the lightweight path; all needs the gateway
origin is resolved from the persisted ScheduleJob.origin — safe on the lightweight scheduler path (it delivers back to the same channel/thread the request came in on). Only all still requires the full BotOS gateway (it enumerates every registered bot); setting all on the lightweight path logs a warning and skips delivery.Multi-user isolation (optional)
Every scheduler store call —list, get_by_name, remove_by_name — accepts an optional principal= argument that scopes the operation to a single resolved end-user identity:
ScheduleJob.principal round-trips through to_dict/from_dict and is omitted when None, so existing on-disk store files are byte-identical.
The
praisonai schedule CLI itself does not yet thread a resolved identity into these calls — a CLI user is effectively “the global principal”. Multi-tenant isolation is enforced when the gateway (or a custom ScheduleStoreProtocol consumer) passes principal= on every call.PM2-Style Daemon Commands
Start Scheduler
With a Task Prompt
With a Recipe
List Schedulers
View Logs
Stop Scheduler
Restart Scheduler
Delete Scheduler
Describe Scheduler
Legacy Foreground Mode
For quick testing or one-off runs, use foreground mode:verbose: true on a scheduled agent enables the Agent’s verbose output mode (equivalent to output: "verbose"). Omit it or set it to false for silent runs.
Tool resolution in scheduled agents.yaml
A scheduled agents.yaml resolves its tools: list through the same ToolResolver that praisonai run uses (via AgentsGenerator._build_tools_dict). Referring to duckduckgo, internet_search, or any other resolver-known tool inside a scheduled config gives the agent that real tool at runtime, so CLI + YAML + Python all behave identically.
Tool parity (PR #3421): Before this change,
create_agent_from_config skipped the resolver on the scheduled path and every YAML entry silently collapsed to a single hardcoded search stub — the scheduled agent was under-equipped compared with its praisonai run counterpart.Ctrl+C to stop. Shows final statistics:
Wall-clock cron & restart recovery
praisonai schedule daemons handle cron: schedules on wall-clock time and survive restarts:
- Time-of-day accuracy —
--interval "cron:0 9 * * *"fires at 09:00 every day in the schedule’s resolved timezone, not at whatever o’clock the daemon started.cron:resolves the zone from the scheduletz→ the instance default →PRAISONAI_SCHEDULE_TIMEZONE, and falls back to UTC when none is set. See Schedule Tools → Timezones. - Downtime catch-up (exactly once) — a slot missed while the daemon was stopped/killed/scaled-to-zero runs once on resume, then re-anchors to the next future occurrence.
- State file —
~/.praisonai/schedulers/<name>.jsonnow carrieslast_run_atalongsideexecutionsandcost.praisonai schedule stop+praisonai schedule startpicks up the schedule exactly where it left off.
cron: schedules use the croniter engine, which is bundled with praisonaiagents (installed transitively when you install praisonai) — no separate install step. The core parse_schedule() refuses a cron schedule at creation with a clear ValueError if the engine is somehow missing on a stripped install, so a cron schedule is never accepted and silently skipped. The praisonai schedule daemon ticker is a separate wrapper path: on a stripped install without croniter it falls back to a coarse interval anchored to process start and logs a one-time warning.
Introduced in PraisonAI PR #3527.
Example — daily morning brief that survives restarts:
Daemon state persistence
The daemon writes catch-up state to~/.praisonai/schedulers/<name>.json. As of PraisonAI #4537:
- Writes are atomic — the state file is written to a temp file,
fsync’d, then renamed into place withos.replace. A crash mid-write can no longer produce a truncated JSON file that silently disables cron catch-up on next start. - Corrupt / unreadable state files log a warning — you’ll see
Corrupt scheduler state ...orCannot access scheduler state ...in the logs, then the daemon re-anchors from process start (previous behaviour with no log).
Storage Locations
- Schedule data:
~/.praisonai/config.yaml(under thescheduleskey) - Log files:
~/.praisonai/logs/*.log
Schedules are stored in the same
config.yaml used by agents and server configuration. Legacy jobs.json data is auto-migrated on first use.Features
✅ PM2-style daemon management - No nohup needed✅ Process persistence - State saved to disk
✅ Easy lifecycle control - start/stop/restart/list
✅ Centralized logging - Auto-rotation, follow mode
✅ Graceful shutdown - SIGTERM with SIGKILL fallback
✅ Cost monitoring - Budget limits with $1.00 default
✅ Timeout protection - Prevent runaway executions
✅ Auto cleanup - Dead processes removed automatically
Schedule Intervals
--interval also accepts natural-language forms — a bare clock time (at 9am) is a one-shot, and any recurring form (every …, daily …, or a day-of-week name) becomes a cron schedule:
Examples
Example 1: Simple Prompt Scheduling
Quick news check every hour:Save Configuration
Command Reference
Daemon Commands
Options
Notes:
- Default budget is $1.00 for safety. Set to higher value or
nullin YAML to disable. - Use
--verboseto see detailed logs. Without it, output is clean for background running.
The CLI
--verbose flag controls scheduler-daemon log verbosity. It is independent of the verbose: key inside agents.yaml, which controls the scheduled Agent’s output mode.Example 2: News Monitoring with YAML (Advanced)
agents.yaml:Example 2: Data Collection (Every 30 Minutes)
agents.yaml:Example 3: With Budget and Timeout Limits
agents.yaml:Example 4: Testing with Short Interval
Python API
For programmatic control, use the async-native Python API:Async Agent Scheduler (Recommended)
Import paths (PR #1552):
- Canonical:
from praisonai.scheduler import AgentScheduler - Deprecated (still works, emits
DeprecationWarning):from praisonai.agent_scheduler import AgentScheduler - Pending deprecation (still works, emits
PendingDeprecationWarning— will move topraisonai.scheduler.async_agent_schedulerin a future release):from praisonai.async_agent_scheduler import AsyncAgentScheduler
AgentScheduler from praisonai.scheduler exposes from_yaml, start_from_yaml_config, and from_recipe. For new applications, prefer AsyncAgentScheduler which provides better cancellation and fits naturally into async codebases.Budget & timeout (PR #1771): AsyncAgentScheduler now accepts timeout (per-run seconds) and max_cost (total USD cap) in its constructor — see Async Agent Scheduler for the full pattern.MCP scheduling note: The MCP server’s praisonai.schedule.list / .add / .remove tools are now backed by praisonaiagents.tools.schedule_tools, so YAML/recipe scheduling works through MCP without needing to choose an import path.Async-only agents (PR #2147): The sync AgentScheduler now accepts agents that expose only astart() (not start()). Dispatch logic is shared between sync and async schedulers via praisonai.scheduler._dispatch.adispatch_agent. Both schedulers prefer astart() when present and fall back to start() in a worker thread.Features
Core Features
- Interval-based scheduling: Run agents at regular intervals
- Background execution: Runs in daemon thread, won’t block terminal
- Automatic retry: Exponential backoff + jitter, capped at 300s, shared between sync & async
- Graceful shutdown: Clean stop with Ctrl+C
- YAML configuration: Simple configuration in agents.yaml
- CLI overrides: Override any setting from command line
Safety Features
- ⏱️ Timeout Protection: Prevent runaway executions
- 💰 Cost Monitoring: Real-time cost tracking with budget limits
- 📊 Statistics Tracking: Monitor execution success rates, costs, and runtime
- 🛡️ Budget Protection: Auto-stops when cost limit reached
- 🔄 Retry Logic: Exponential backoff prevents rapid failures
Real cost tracking (PR #2171):
--max-cost is now enforced against the real per-token spend pulled from each agent response. Previously the scheduler added a fixed $0.0001 per run, so the default $1.00 brake required ~10,000 executions to trip. Set --max-cost to your actual budget, and ensure your model is in DEFAULT_PRICING (or register it — see Cost Tracking) so runs aren’t silently priced at $0.Output
The scheduler provides detailed logging with cost tracking:Callbacks
Both schedulers accepton_success and on_failure callbacks in the constructor.
Callbacks may be sync or async functions; a raising callback is logged and swallowed
— it will not stop the scheduler.
on_success(result)— called with the agent’s return value after a successful run.on_failure(exc)— called with the finalExceptionafter all retries are exhausted. (Previously sync passed a formatted string; as of PR #1474 both sync and async pass the exception object.)
Statistics
CLI Daemon: Usepraisonai schedule describe <name> for detailed stats
Python API (AsyncAgentScheduler):
SUPPRESSED and NOT_CONFIGURED runs count as successful_executions but do not count as either delivered_deliveries or undelivered_deliveries — each counter is bumped only by its own outcome. See the per-outcome counter table in Scheduler Delivery.
Stats parity (PR #1857): Both
AgentScheduler and AsyncAgentScheduler now return the same stats shape via a shared _BaseAgentScheduler._build_stats() helper. Keys: is_running, total_executions, successful_executions, failed_executions, success_rate, total_cost_usd, remaining_budget, runtime_seconds, cost_per_execution, delivered_deliveries, undelivered_deliveries. Use await scheduler.get_stats_async() in async code, scheduler.get_stats() in sync code.On stop (Ctrl+C)
🛑 Stopping scheduler… 📊 Final Statistics: Total Executions: 5 Successful: 5 Failed: 0 Success Rate: 100.0% ✅ Agent stopped successfullyCLI Commands
Use the daemon management commands:Python API
Foreground Mode
PressCtrl+C to stop gracefully. The scheduler will:
- Set stop event
- Wait for current execution to complete
- Log final statistics
- Exit cleanly
Troubleshooting
TypeError: Agent.__init__() got an unexpected keyword argument 'verbose'
The scheduler’s YAML loader used to forward verbose: to Agent(...) as a keyword argument, but Agent does not accept verbose. Every scheduled agents.yaml — with or without a verbose: key — raised this error on startup.
Fix: upgrade praisonai to a version that includes PR #4348. After the fix, verbose: true in YAML is translated to Agent(output="verbose") internally; false (or omitted) becomes output="silent".
See Also
- Async Agent Scheduler - Python async-native scheduler API
- Delivery Config - The
DeliveryRoutermachinery scheduled results reuse (origin/allneed the full gateway) - Scheduled Run Policy - Tool scoping, prompt scanning, and output auditing for unattended runs (configure via the
scheduler:block ingateway.yaml, or viaRunPolicyin Python for non-gateway embeddings) - Planning Mode - Add planning to scheduled agents
- Memory - Enable memory for scheduled agents
- Tools - Add custom tools to agents
- Examples - Working examples

