Scheduler CLI
Schedule agents to run continuously 24/7 at regular intervals
The Scheduler CLI enables 24/7 autonomous agent operations by running agents at regular intervals.
Quick Start
With Direct Prompt (No YAML needed)
# Schedule with a simple prompt
praisonai schedule "Check AI news and summarize" --interval hourlyWith agents.yaml
# Schedule using agents.yaml configuration
praisonai schedule agents.yamlInstallation
pip install praisonai praisonaiagents
export OPENAI_API_KEY=your_key_hereDeliver 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:
Python
from praisonaiagents import Agent
from praisonai.scheduler import AgentScheduler
agent = Agent(name="Briefer", instructions="Summarise this morning's news in 5 bullets.")
AgentScheduler(agent, task="Morning brief", deliver="telegram:123456").start("every 1h")YAML
schedule:
every: "1h" # alias for `interval`
deliver: "telegram:123456"CLI
praisonai schedule agent.yaml --interval 1h --deliver telegram:123456
# or
praisonai schedule agent.yaml -i 1h -d telegram:123456Token Grammar
The deliver token uses DeliveryTarget.parse. Whitespace is stripped and origin/all are case-insensitive.
| Token | Meaning |
|---|---|
"" / omitted | No delivery (default) |
telegram | Platform’s home channel (router resolves) |
telegram:123456 | Explicit channel |
telegram:123:789 | Explicit channel + thread (platform:channel_id:thread_id) |
origin | Persisted job origin — resolves on the lightweight path when ScheduleJob.origin is set; otherwise logs a warning and skips |
all | Fan out to every registered bot — requires the full BotOS gateway |
Optional Dependency
Delivery goes through the praisonai-bot package.
pip install "praisonai[bot]"If it isn’t installed, the scheduled run still succeeds — a single warning is logged and delivery is a no-op.
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.
| Outcome | When it fires | Callback | Counted as |
|---|---|---|---|
DELIVERED | Router accepted the send | on_success(result) | delivered_deliveries |
SUPPRESSED | Whole output is exactly NO_REPLY / [SILENT] / SILENT | on_success(result) | Neither (recorded non-outcome) |
NOT_CONFIGURED | No deliver= target was set | on_success(result) | Neither |
UNDELIVERED | deliver=True returned False, wrapper couldn’t build (e.g. praisonai-bot missing), token unresolvable, delivery raised | on_failure("scheduled result could not be delivered") | undelivered_deliveries |
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:
# crontab -e
TELEGRAM_BOT_TOKEN=123456:AAE...
TELEGRAM_HOME_CHANNEL=-100555
0 8 * * * cd /srv/praisonai && praisonai schedule tickOr 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.
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().
| Outcome | When it fires | Callback | Counted as |
|---|---|---|---|
DELIVERED | Router accepted the send | on_success(result) | delivered_deliveries |
SUPPRESSED | Whole output is exactly NO_REPLY / [SILENT] / SILENT | on_success(result) | Neither (recorded non-outcome) |
NOT_CONFIGURED | No deliver= target was set | on_success(result) | Neither |
UNDELIVERED | deliver=True returned False, wrapper couldn’t build, token unresolvable, delivery raised | on_failure("scheduled result could not be delivered") | undelivered_deliveries |
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.
import asyncio
from praisonaiagents import Agent
from praisonai.scheduler import AsyncAgentScheduler
async def main():
agent = Agent(name="Briefer", instructions="Summarise today's AI news in 5 bullets.")
scheduler = AsyncAgentScheduler(agent=agent, task="Morning brief", deliver="telegram:123456")
await scheduler.start("hourly")
asyncio.run(main())Delivery Best Practices
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
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 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:
from praisonaiagents.scheduler import get_default_store
store = get_default_store()
# Only Alice's jobs
alice_jobs = store.list(principal="telegram:12345")
# Cross-owner lookup / delete are refused
store.get_by_name("morning-brief", principal="bob") # -> None if owned by alice
store.remove_by_name("morning-brief", principal="bob") # -> False; job intactScheduleJob.principal round-trips through to_dict/from_dict and is omitted when None, so existing on-disk store files are byte-identical.
PM2-Style Daemon Commands
Start Scheduler
With a Task Prompt
# Start as background daemon
praisonai schedule start <name> "Your task" --interval hourly
# With all options
praisonai schedule start my-agent "Monitor logs" \
--interval "*/30m" \
--timeout 120 \
--max-cost 2.00 \
--max-retries 3
# Examples
praisonai schedule start news-bot "Check AI news" --interval hourly
praisonai schedule start health-check "Monitor system" --interval "*/15m"
praisonai schedule start test-agent "Count to 5" --interval "*/10s"With a Recipe
# Schedule a recipe
praisonai schedule start news-monitor --recipe news-analyzer --interval hourly
# Recipe with custom interval
praisonai schedule start daily-report --recipe report-generator --interval daily
# Recipe with all options
praisonai schedule start my-scheduler \
--recipe my-recipe \
--interval "*/6h" \
--timeout 600 \
--max-cost 2.00 \
--max-retries 3List Schedulers
# Show all running schedulers
praisonai schedule list
# Output example:
# Name Status PID Interval Task
# ================================================================
# news-bot 🟢 running 12345 hourly Check AI news
# health-check 🟢 running 12346 */15m Monitor system
#
# Total: 2 scheduler(s)View Logs
# View last 50 lines
praisonai schedule logs <name>
# Follow logs in real-time
praisonai schedule logs <name> -f
# Examples
praisonai schedule logs news-bot
praisonai schedule logs news-bot --followStop Scheduler
# Graceful shutdown
praisonai schedule stop <name>
# Example
praisonai schedule stop news-botRestart Scheduler
# Restart with same configuration
praisonai schedule restart <name>
# Example
praisonai schedule restart news-botDelete Scheduler
# Remove from list (must be stopped first)
praisonai schedule delete <name>
# Example
praisonai schedule delete news-botDescribe Scheduler
# Show detailed information
praisonai schedule describe <name>
# Shows: PID, status, uptime, executions, cost, config, logs path
# Note: Async daemons (schedule start) now correctly persist executions and cost to disk (PR #1857)Legacy Foreground Mode
For quick testing or one-off runs, use foreground mode:
# Direct prompt (runs in foreground)
praisonai schedule "Your task" --interval hourly
# YAML mode (runs in foreground)
praisonai schedule agents.yamlYAML Configuration Example:
framework: praisonai
agents:
- name: "AI News Monitor"
role: "Technology News Analyst"
goal: "Monitor and summarize AI news"
instructions: "Search for latest AI developments"
tools:
- search_tool
verbose: true
task: "Search for latest AI news and provide top 3 stories"
schedule:
interval: "hourly"
max_retries: 3
run_immediately: true
timeout: 60
max_cost: 1.00verbose: 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.
# scheduled_agent.yaml — schedule via `praisonai schedule ...`
agents:
- name: NewsChecker
instructions: Summarise today's AI news in three bullet points
tools:
- duckduckgo # resolved via ToolResolver, not the old hardcoded search_tool
- internet_searchRun YAML in foreground:
praisonai schedule agents.yamlPress Ctrl+C to stop. Shows final statistics:
Execution stats - Total: 12, Success: 11, Failed: 1
Total cost: $0.0056
Runtime: 3600.5sWall-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:
praisonai schedule start morning-brief "Summarise today's news in 5 bullets" \
--interval "cron:0 9 * * *" \
--deliver telegram:123456Stop 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 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).
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.
The schedule store (FileScheduleStore) has matching crash-safe guarantees for schedule data and run history — see Async Scheduler → Store write durability.
Storage Locations
- Schedule data:
~/.praisonai/config.yaml(under thescheduleskey) - Log files:
~/.praisonai/logs/*.log
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
| Format | Interval | Description |
|---|---|---|
hourly | 3600s | Every hour |
daily | 86400s | Every 24 hours |
weekly | 604800s | Every 7 days |
*/30m | 1800s | Every 30 minutes |
*/6h | 21600s | Every 6 hours |
*/5s | 5s | Every 5 seconds (testing) |
3600 | 3600s | Custom seconds |
cron:0 9 * | wall-clock | Fires at real time-of-day. Uses the croniter engine bundled with praisonaiagents. |
weekdays at 9am | wall-clock | Natural language → cron:0 9 1-5. Mon–Fri. |
every day at 8am | wall-clock | Natural language → cron:0 8 *. Daily. |
--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:
praisonai schedule "Post standup" --interval "weekdays at 9am"
praisonai schedule "Send digest" --interval "every day at 8am"See Async Agent Scheduler → Natural language for the full grammar.
Examples
Example 1: Simple Prompt Scheduling
Quick news check every hour:
praisonai schedule "Search for latest AI news and summarize top 3 stories" --interval hourly --verboseSystem monitoring every 15 minutes:
praisonai schedule "Check system health and disk space" --interval "*/15m"With budget limit:
praisonai schedule "Analyze market trends" \
--interval "*/30m" \
--max-cost 0.50 \
--timeout 120Save Configuration
# Export scheduler config to YAML
praisonai schedule save <name> [output.yaml]
# Example
praisonai schedule save news-bot news-config.yamlCommand Reference
Daemon Commands
| Command | Description | Example |
|---|---|---|
start <name> "task" | Start scheduler as daemon | praisonai schedule start my-bot "Task" |
list | List all schedulers | praisonai schedule list |
logs <name> [--follow] | View logs | praisonai schedule logs my-bot --follow |
stop <name> | Stop scheduler | praisonai schedule stop my-bot |
restart <name> | Restart scheduler | praisonai schedule restart my-bot |
delete <name> | Remove from list | praisonai schedule delete my-bot |
describe <name> | Show details | praisonai schedule describe my-bot |
save <name> [file] | Export to YAML | praisonai schedule save my-bot config.yaml |
Options
| Option | Type | Description | Default | Example |
|---|---|---|---|---|
--interval | string | Schedule interval | hourly | hourly, daily, weekly, */30m |
--max-retries | int | Max retry attempts | 3 | 3, 5 |
--timeout | int | Max execution time (seconds) | None | 60, 120 |
--max-cost | float | Budget limit in USD | $1.00 | 1.00, 5.00 |
--deliver, -d | string | Deliver each result to a chat target (overrides YAML/recipe) | "" | telegram:123456, telegram |
--verbose, -v | flag | Enable verbose logging | False | – |
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.
Example 2: News Monitoring with YAML (Advanced)
agents.yaml:
framework: praisonai
agents:
- name: "AI News Monitor"
role: "Technology News Analyst"
instructions: "Search and summarize latest AI news"
tools:
- search_tool
task: "Search for latest AI news and provide top 3 stories"
schedule:
interval: "hourly"
max_retries: 3
run_immediately: trueRun:
praisonai schedule agents.yamlExample 2: Data Collection (Every 30 Minutes)
agents.yaml:
framework: praisonai
agents:
- name: "Data Collector"
role: "Data Analyst"
instructions: "Collect and analyze market data"
tools:
- search_tool
task: "Collect latest market data and identify trends"
schedule:
interval: "*/30m"
max_retries: 5
run_immediately: falseRun with override:
# Override to run every 15 minutes instead
praisonai schedule agents.yaml --interval "*/15m"Example 3: With Budget and Timeout Limits
agents.yaml:
framework: praisonai
agents:
- name: "Budget-Controlled Agent"
role: "Worker"
instructions: "Process data efficiently"
tools:
- search_tool
task: "Process and analyze data"
schedule:
interval: "*/5m"
max_retries: 3
run_immediately: true
timeout: 120 # Max 2 minutes per execution
max_cost: 0.50 # Stop after $0.50 spentRun:
praisonai schedule agents.yaml --verboseOutput:
Budget limit: $0.50
Timeout per execution: 120s
...
Run cost: $0.0034 (model=gpt-4o-mini, in=2100, out=480). Total: $0.0034 / $0.5
...
Budget limit reached: $0.5001 >= $0.50
Stopping scheduler to prevent additional costsExample 4: Testing with Short Interval
# Test with 10-second interval
praisonai schedule agents.yaml --interval "*/10s" --verbosePython API
For programmatic control, use the async-native Python API:
Async Agent Scheduler (Recommended)
import asyncio
from praisonaiagents import Agent
from praisonai.scheduler import AgentScheduler
async def main():
# Create agent
agent = Agent(
name="NewsChecker",
instructions="Check latest AI news",
)
# Create scheduler using canonical import
scheduler = AgentScheduler(
agent=agent,
task="Search for latest AI news"
)
# Start (runs every hour)
scheduler.start("hourly", max_retries=3, run_immediately=True)
# Keep running
try:
import time
time.sleep(3600) # Run for 1 hour
except KeyboardInterrupt:
pass
finally:
scheduler.stop()
stats = scheduler.get_stats()
print(f"Executions: {stats['execution_count']}, Success: {stats['success_count']}")
asyncio.run(main())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 — enforced on the scheduled loop,
run_immediately=True, andexecute_once()(PR #5766). - 🔄 Retry Logic: Exponential backoff prevents rapid failures
Output
The scheduler provides detailed logging with cost tracking:
Starting agent scheduler: AI News Monitor
Task: Search for latest AI news
Schedule: hourly (3600s interval)
# For a cron schedule the line reads instead:
# Schedule: cron:0 9 * * * (wall-clock cron)
Timeout per execution: 60s
Budget limit: $1.00
Agent scheduler started successfully
[2025-12-22 10:00:00] Starting scheduled agent execution
Attempt 1/3
Agent execution successful on attempt 1
Result: [agent output]
Run cost: $0.0034 (model=gpt-4o-mini, in=2100, out=480). Total: $0.0034 / $1.0
Next execution in 3600 seconds (1.0 hours)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 finalExceptionafter all retries are exhausted.- (Previously sync passed a formatted string; as of PR #1474 both sync and async
- pass the exception object.)
def success_callback(result):
print(f"Success: {result}")
def failure_callback(exception):
print(f"Failed: {exception}")
scheduler = AgentScheduler(
agent=agent,
task="Check news",
on_success=success_callback,
on_failure=failure_callback
)Statistics
CLI Daemon: Use praisonai schedule describe <name> for detailed stats
Python API (AsyncAgentScheduler):
stats = await scheduler.get_stats_async()
# Returns:
{
"is_running": True,
"total_executions": 10,
"successful_executions": 9,
"failed_executions": 1,
"success_rate": 90.0,
"total_cost_usd": 0.0234,
"remaining_budget": 0.9766,
"runtime_seconds": 3601.5,
"cost_per_execution": 0.0023,
"delivered_deliveries": 8,
"undelivered_deliveries": 1,
}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.
On stop (Ctrl+C)
🛑 Stopping scheduler…
📊 Final Statistics: Total Executions: 5 Successful: 5 Failed: 0 Success Rate: 100.0%
✅ Agent stopped successfully
## Error Handling
The scheduler retries failed executions with exponential backoff + jitter:
- **Attempt 1:** Run immediately.
- **On failure:** Wait `backoff_delay(attempt)` seconds, then retry.
- Delay formula: `min(max(30, 2 ** attempt), 300)` seconds, with ±10% jitter.
- **Cap:** 300s between attempts. **Floor:** ~27s.
- Sync and async schedulers now share the same `backoff_delay()` implementation as of [PR #1673](https://github.com/MervinPraison/PraisonAI/pull/1673), not just the same formula.
`max_retries` is the **total** number of attempts (including the first run).
`max_retries=3` → 1 initial attempt + up to 2 retries.
## Stopping the Scheduler
### Interruptible backoff (PR #1673)
Calling `scheduler.stop()` during a retry backoff window now returns within milliseconds. Previously a stop signal could wait out the full backoff delay (up to ~300s).
The async scheduler reached the same parity in [PraisonAI #4537](https://github.com/MervinPraison/PraisonAI/pull/4537) — see [Async Scheduler → Graceful Shutdown](/features/async-scheduler#best-practices).
### Cross-platform timeout (PR #1741)
Execution timeouts now work on all platforms including Windows and can run safely in worker threads (FastAPI background tasks, the bot adapter, Streamlit). Previously timeouts were Unix/Linux/Mac only and failed in worker thread contexts.
```python
import signal
from praisonai.scheduler import AgentScheduler
scheduler = AgentScheduler(agent, "Monitor logs")
scheduler.start("hourly", max_retries=3)
# Set up signal handler
def handle_sigint(signum, frame):
print("Ctrl+C pressed - stopping scheduler...")
scheduler.stop() # Returns immediately, even during backoff
exit(0)
signal.signal(signal.SIGINT, handle_sigint)CLI Commands
Use the daemon management commands:
# Graceful shutdown
praisonai schedule stop <name>
# Force kill if needed
praisonai schedule delete <name>Python API
# Graceful async shutdown
await scheduler.stop() # Waits up to 30s for current executionForeground Mode
Press Ctrl+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