Since PraisonAI PR #1566, exceptions inside a scheduled run are caught and reported via
on_failure without killing the scheduler loop. Use await scheduler.get_stats_async() for metrics from async code.Agent-scheduler runs inherit the same at-most-once claim behavior when the underlying store supports it — a due job fires on only one worker per tick even with several processes polling the same store. See Multi-process safety.
Quick Start
How It Works
The AsyncAgentScheduler uses async-native execution with cooperative cancellation, replacing the old thread-based scheduler.The scheduler’s async primitives (
_stop_event, _cancel_event, _stats_lock) are now created lazily inside _ensure_async_primitives() and bound to the loop that start() runs on. Tests that call stop() without first calling start() must invoke scheduler._ensure_async_primitives() explicitly — see tests/unit/scheduler/test_async_agent_scheduler.py in PR #1583 for the canonical pattern.Shared dispatch helper (PR #2147): AsyncPraisonAgentExecutor.execute() now delegates to the shared adispatch_agent helper (praisonai.scheduler._dispatch). No behaviour change — the dispatch ladder (astart → to_thread(start) → AttributeError) is unchanged. See also the sync scheduler note for the corresponding fix to AgentScheduler.Schedule Expression Reference
| Expression | Interval | Description |
|---|---|---|
"daily" | 86400s | Every 24 hours |
"hourly" | 3600s | Every hour |
"*/30m" | 1800s | Every 30 minutes |
"*/1h" | 3600s | Every 1 hour |
"*/5s" | 5s | Every 5 seconds |
"60" | 60s | Custom seconds (plain digits) |
Configuration Options
AsyncAgentScheduler Constructor
| Parameter | Type | Default | Description |
|---|---|---|---|
agent | Any | Required | Agent instance to schedule |
task | str | Required | Task description to execute |
config | Optional[Dict[str, Any]] | None | Optional configuration dictionary |
on_success | Optional[Callable[[Any], None]] | None | Callback function on successful execution |
on_failure | Optional[Callable[[Exception], None]] | None | Callback function on failed execution |
timeout | Optional[int] | None | NEW — Maximum execution time per run in seconds. None means no limit. Enforced with asyncio.wait_for(). |
max_cost | Optional[float] | 1.00 | NEW — Maximum total cost in USD. Scheduler auto-stops when reached. Set to None to disable. Default $1.00 is a safety guard. |
start() Method Options
| Parameter | Type | Default | Description |
|---|---|---|---|
schedule_expr | str | Required | Schedule expression (e.g., “hourly”, ”*/1h”, “3600”) |
max_retries | int | 3 | Maximum retry attempts on failure |
run_immediately | bool | False | If True, run agent immediately before starting schedule |
Reading Stats
Statistics can be read in both sync and async contexts with different guarantees:| Method | Sync/Async | Atomicity | When to Use |
|---|---|---|---|
scheduler.get_stats() | sync | Best-effort, no lock — counters may be observed mid-update during concurrent execution. | Backward compatibility. Quick stats access in sync contexts. |
await scheduler.get_stats_async() | async | Atomic snapshot under _stats_lock — all counters read together. | Async contexts where consistent stats are needed. |
scheduler.get_stats_sync() | sync | Best-effort, no lock — same as get_stats() for clarity. | Explicit sync contexts (tests, scripts, REPL). |
Stats Response Format
| Field | Type | Description |
|---|---|---|
is_running | bool | Whether scheduler is currently running |
total_executions | int | Total number of execution attempts |
successful_executions | int | Number of successful executions |
failed_executions | int | Number of failed executions |
success_rate | float | Success percentage (0-100) |
total_cost_usd | float | Running total cost in USD (4 dp) |
remaining_budget | float | None | max_cost - total_cost_usd, or None if max_cost is disabled |
runtime_seconds | float | NEW (PR #1857) — Seconds since start() was called. 0 when scheduler hasn’t started. |
cost_per_execution | float | NEW (PR #1857) — total_cost_usd / total_executions, rounded to 4 dp. 0 when no executions yet. |
AsyncAgentScheduler shares its stats builder with the sync AgentScheduler via the internal _BaseAgentScheduler mixin — the schema is now identical across sync and async.
Common Patterns
Running in FastAPI Application
Error Handling with Logging
Graceful Shutdown on SIGINT
Budget-aware Scheduling
Real cost tracking (PR #2171): Before #2171, both schedulers added a fixed
$0.0001 to total_cost per run, so the default max_cost=1.00 only tripped after ~10,000 runs regardless of model. The scheduler now pulls usage (input/output tokens) and model off the agent response and prices it through praisonai.cli.features.cost_tracker.ModelPricing. Responses with no usage metadata contribute $0 — the brake errs on the side of running rather than tripping on missing data. Negative token counts are clamped to 0 so they can never bypass the brake.Daemon state persistence
WhenAsyncAgentScheduler runs as a daemon (started via praisonai schedule start <name> ...), it now updates ~/.praisonai/schedulers/<name>.json after every execution with the current executions count and cost. The file I/O is offloaded with asyncio.to_thread() so the event loop is never blocked. Previously these writes were a TODO and async daemons silently reported stale numbers. See PR #1857.
Best Practices
Always await scheduler.stop() before exiting
Always await scheduler.stop() before exiting
The
stop() method waits up to 30 seconds for the current execution to complete before canceling. This prevents data corruption and ensures clean shutdown.Use run_immediately=True for testing
Use run_immediately=True for testing
Enable
run_immediately=True to verify your agent works correctly before waiting for the first scheduled interval.Keep callbacks lightweight
Keep callbacks lightweight
Success and failure callbacks are called synchronously. Heavy operations should be offloaded to avoid blocking the scheduler.
Prefer AsyncAgentScheduler over legacy thread-based scheduler
Prefer AsyncAgentScheduler over legacy thread-based scheduler
For new code, use
AsyncAgentScheduler instead of the legacy AgentScheduler. The async version provides better cancellation, no daemon threads, and fits naturally into async applications.Budget Control
Budget Control
The default When the budget triggers, the scheduler logs a warning and calls
max_cost=1.00 caps unattended cost runaway. Since PR #2171, total_cost_usd is the real per-token spend computed from each response’s usage field — pick a value that matches your approved budget, not a multiple chosen to compensate for an undercount.stop_event.set() internally — stats["is_running"] flips to False.If your model is missing from DEFAULT_PRICING, the run is priced at $0 and total_cost_usd will under-report — register it via the custom pricing snippet so the brake stays meaningful.Timeout Configuration
Timeout Configuration
Set
timeout to bound the worst-case wall-clock time per run. Internally implemented with asyncio.wait_for(), which raises asyncio.TimeoutError and triggers the standard retry path.timeout=None (the default) imposes no limit.Import paths (updated in PR #1723):
- Canonical (recommended):
from praisonai.scheduler import AsyncAgentScheduler - Explicit module path:
from praisonai.scheduler.async_agent_scheduler import AsyncAgentScheduler - Deprecated (still works, emits
DeprecationWarning):from praisonai.async_agent_scheduler import AsyncAgentScheduler
AgentScheduler is unchanged: from praisonai.scheduler import AgentScheduler.Skipping Ticks with a Pre-Run Gate
A pre-run gate lets you run a cheap shell check before each scheduled tick — the agent only fires when the check says there’s something to do. This cuts token spend on quiet ticks (e.g. polling for new emails every 5 minutes but only summarising when mail actually arrives). See Scheduler Pre-Run Gate for the full configuration reference and examples.Related
RunPolicy adds run-scoped guardrails (tool scoping, prompt scanning, durable audit) to ScheduledAgentExecutor. AsyncAgentScheduler is independent of RunPolicy — see Scheduled Run Policy for how to add guardrails when using ScheduledAgentExecutor or EnhancedScheduledAgentExecutor.AsyncAgentScheduler lives in the praisonai wrapper (from praisonai.scheduler import AsyncAgentScheduler). ScheduledAgentExecutor and JobResult live in the bot tier — use from praisonai_bot.scheduler import ScheduledAgentExecutor, JobResult when using them directly.Scheduler CLI
Command-line interface for scheduling agents
Pre-Run Gate
Skip ticks when a cheap check says nothing to do
Background Tasks
Running agents as background processes
Run Policy
Scope tools, scan prompts, and audit output for unattended runs

