Skip to main content
The Scheduler CLI enables 24/7 autonomous agent operations by running agents at regular intervals.

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 a deliver token from Python, YAML, or the CLI:

Token Grammar

The deliver token uses DeliveryTarget.parse. Whitespace is stripped and origin/all are case-insensitive.

Optional Dependency

Delivery goes through the praisonai-bot package.
If it isn’t installed, the scheduled run still succeeds — a single warning is logged and delivery is a no-op.
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 through on_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:
Or drop the 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 fires on_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

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.
Store schedule.deliver in YAML so scheduled jobs are reproducible. Override with -d only for one-off runs.
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:
YAML Configuration Example:
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.
Run YAML in foreground:
Press 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 schedule tz → 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>.json now carries last_run_at alongside executions and cost. praisonai schedule stop + praisonai schedule start picks 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:
Stop the daemon at 14:00, restart at 15:00 — today’s 09:00 slot already ran (so no catch-up), and the next fire is tomorrow 09:00, not 15:00 + 24h.

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 with os.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 ... or Cannot access scheduler state ... in the logs, then the daemon re-anchors from process start (previous behaviour with no log).
If a scheduled run misses its slot after a crash, grep the logs for those two messages first. The async scheduler shares the same behaviour — see Async Scheduler → Daemon state persistence.

Storage Locations

  • Schedule data: ~/.praisonai/config.yaml (under the schedules key)
  • 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:
See Async Agent Scheduler → Natural language for the full grammar.

Examples

Example 1: Simple Prompt Scheduling

Quick news check every hour:
System monitoring every 15 minutes:
With budget limit:

Save Configuration

Command Reference

Daemon Commands

Options

Notes:
  • Default budget is $1.00 for safety. Set to higher value or null in YAML to disable.
  • Use --verbose to 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:
Run:

Example 2: Data Collection (Every 30 Minutes)

agents.yaml:
Run with override:

Example 3: With Budget and Timeout Limits

agents.yaml:
Run:
Output:

Example 4: Testing with Short Interval

Python API

For programmatic control, use the async-native Python API:
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 to praisonai.scheduler.async_agent_scheduler in a future release): from praisonai.async_agent_scheduler import AsyncAgentScheduler
The canonical 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 accept on_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 final Exception after 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: Use praisonai 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 successfully

CLI Commands

Use the daemon management commands:

Python API

Foreground Mode

Press Ctrl+C to stop gracefully. The scheduler will:
  1. Set stop event
  2. Wait for current execution to complete
  3. Log final statistics
  4. 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 DeliveryRouter machinery scheduled results reuse (origin / all need the full gateway)
  • Scheduled Run Policy - Tool scoping, prompt scanning, and output auditing for unattended runs (configure via the scheduler: block in gateway.yaml, or via RunPolicy in 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