Quick Start
How It Works
Multi-process deployments: When more than one
BotOS process shares the same schedule store, each due job is claimed atomically before running and fires at most once across all processes — provided the store supports claim_due (the default FileScheduleStore does). See BotOS → Multi-Process / HA Deployments.| Phase | Description |
|---|---|
| Initialization | Creates async primitives lazily on first use |
| Scheduling | Runs agent at specified intervals with exponential backoff |
| Execution | Uses thread pool for sync agents, direct await for async agents |
| Cancellation | Cooperative cancellation via asyncio.Event |
Multi-process safety (at-most-once)
Without an atomic claim, two workers polling the same store could each fire the same due job — causing double-posts, double-charges, or duplicated side effects. Since PraisonAI PR #2628,FileScheduleStore claims each due job under an OS advisory file lock (fcntl on POSIX, msvcrt on Windows) and hands it back to only one worker per tick. Losers receive an empty list and skip silently.
Under the lock the store re-reads on-disk state, pre-advances last_run_at, and takes a short lease atomically. One-shot jobs are removed on claim. An unfinished claim releases automatically once its lease expires — this is the crash-safety mechanism.
| Concept | What it is | Default |
|---|---|---|
owner_id | Unique per-worker identifier | f"{hostname}:{pid}:{uuid}" (auto) |
lease_seconds | How long a claim is held before it can be re-claimed | 300.0 |
supports_atomic_claim() | Runner feature-detect flag; when False, the wrapper falls back to non-atomic get_due_jobs | auto-detected from the store |
ScheduledAgentExecutor.tick() does this automatically: it claims under the lock when the store supports it (default owner_id = f"{hostname}:{pid}:{uuid}"), releases the lease after each run, and falls back to get_due_jobs otherwise.
Configuration Options
AsyncAgentScheduler API Reference
Complete parameter documentation and examples
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
agent | Any | Required | Agent instance to schedule |
task | str | Required | Task description to execute |
config | Dict[str, Any] | {} | Optional configuration |
on_success | Callable | None | Success callback (sync or async) |
on_failure | Callable | None | Failure callback (sync or async) |
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 Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
schedule_expr | str | Required | Schedule interval expression |
max_retries | int | 3 | Total attempts (1 initial + retries) |
run_immediately | bool | False | Execute immediately before scheduling |
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. |
Common Patterns
Pattern 1: Event Loop Safety
The scheduler is safe to construct outside an event loop:Pattern 2: FastAPI Integration
Pattern 3: Cancellation Handling
Pattern 4: 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.Best Practices
Event Loop Safety
Event Loop Safety
Always create async primitives lazily. The scheduler binds
asyncio.Event and asyncio.Lock to the caller’s loop on first async entry, preventing “different loop” errors.Callback Best Practices
Callback Best Practices
Use both sync and async callbacks safely. The
safe_call utility handles both types automatically.Retry Strategy
Retry Strategy
Use exponential backoff with jitter. Both sync and async schedulers share the same
backoff_delay algorithm for consistency.Graceful Shutdown
Graceful Shutdown
Always await
stop() for proper cleanup and statistics reporting.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.Multi-process lease tuning
Multi-process lease tuning
Pick a Don’t share a
lease_seconds long enough to cover your slowest job, but short enough to recover quickly after a crash — an unfinished claim only re-runs after its lease expires.FileScheduleStore path across hosts unless the filesystem honors the lock (NFS does not). Leave owner_id on its auto default (f"{hostname}:{pid}:{uuid}") unless you need a stable value for observability.Import paths (PR #1552):
- Pending deprecation (still works, emits
PendingDeprecationWarning— will move topraisonai.scheduler.async_agent_schedulerin a future release):from praisonai.scheduler import AsyncAgentScheduler - Canonical:
from praisonai.scheduler import AgentScheduler(sync scheduler) - Deprecated (still works, emits
DeprecationWarning):from praisonai.agent_scheduler import AgentScheduler
AgentScheduler exposes from_yaml, start_from_yaml_config, and from_recipe, which the deprecated top-level shim does not advertise but now re-exports correctly.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.Skipping Ticks with a Pre-Run Gate
A pre-run gate runs a cheap shell command before each tick and skips the model turn when the check says there’s nothing to do — zero tokens spent on quiet ticks. See Scheduler Pre-Run Gate for configuration options and examples.Related
Sync Agent Scheduler
Thread-based scheduler for sync environments
Pre-Run Gate
Skip ticks when a cheap check says nothing to do
Shared Scheduler Utilities
Common primitives used by both schedulers

