Skip to main content
Run an agent on a recurring schedule with async-native execution, cooperative cancellation, and built-in retries.
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.
The user sets a recurring schedule; the scheduler runs the agent asynchronously without blocking the event loop.

Quick Start

1

Simple Usage

2

With Callbacks

3

With Timeout & Budget Limit

4

Deliver Results to a Chat Channel

AsyncAgentScheduler accepts the same deliver= parameter as the sync scheduler and dispatches delivery through asyncio.to_thread — the shared helper uses the sync bridge, so delivery never blocks the event loop and never raises. See Scheduler → Deliver Results for the full token grammar and the praisonai[bot] optional dependency.

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 (astartto_thread(start)AttributeError) is unchanged. See also the sync scheduler note for the corresponding fix to AgentScheduler.

Schedule Expression Reference

Since PraisonAI PR #2916, the wrapper delegates the shared grammar (hourly / daily / weekly / */N{m,h,s} / raw seconds) to praisonaiagents.scheduler.parser.parse_schedule, so weekly is now accepted by every wrapper scheduler (AgentScheduler, AsyncAgentScheduler, DeploymentScheduler, and the praisonai schedule CLI). The wrapper-specific cron: collapse and the bare */N (unit-less) fast-path stay local to the wrapper.

Configuration Options

AsyncAgentScheduler Constructor

start() Method Options

Reading Stats

Statistics can be read in both sync and async contexts with different guarantees:

Stats Response Format

Since PR #1857, 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

When AsyncAgentScheduler 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

The stop() method waits up to 30 seconds for the current execution to complete before canceling. This prevents data corruption and ensures clean shutdown.
Enable run_immediately=True to verify your agent works correctly before waiting for the first scheduled interval.
Success and failure callbacks are called synchronously. Heavy operations should be offloaded to avoid blocking the 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.
The default 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.
When the budget triggers, the scheduler logs a warning and calls 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.
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
The sync AgentScheduler is unchanged: from praisonai.scheduler import AgentScheduler.
Jupyter/Event Loop Compatibility: Starting with PR #1448, PraisonAI no longer calls nest_asyncio.apply() or asyncio.set_event_loop() on your behalf when ACP/LSP is enabled. If you embed PraisonAI inside a Jupyter kernel or another running event loop, either call nest_asyncio.apply() yourself at the top of your notebook, or run PraisonAI from a separate process.

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.

Delivering Results to a Chat

Pass deliver= to push each successful result to Telegram, Discord, Slack, or WhatsApp — no gateway required.
See Scheduler Delivery for the token grammar (Python / YAML / CLI) and reliability guarantees.
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

Scheduler Delivery

Push scheduled results to Telegram/Discord/Slack/WhatsApp

Background Tasks

Running agents as background processes

Run Policy

Scope tools, scan prompts, and audit output for unattended runs