> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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)

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Schedule with a simple prompt
praisonai schedule "Check AI news and summarize" --interval hourly
```

### With agents.yaml

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Schedule using agents.yaml configuration
praisonai schedule agents.yaml
```

## Installation

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
pip install praisonai praisonaiagents
export OPENAI_API_KEY=your_key_here
```

## Deliver Scheduled Results to a Chat Channel

Route each successful scheduled run to Telegram, Discord, Slack, or WhatsApp — no gateway required.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[🤖 Agent] --> B[⏰ AgentScheduler<br/>deliver=telegram:123456]
    B --> C[📮 DeliveryRouter]
    C --> D[✅ Telegram]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff

    class A,B agent
    class C tool
    class D success
```

Set a `deliver` token from Python, YAML, or the CLI:

<Tabs>
  <Tab title="Python">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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")
    ```
  </Tab>

  <Tab title="YAML">
    ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    schedule:
      every: "1h"           # alias for `interval`
      deliver: "telegram:123456"
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    praisonai schedule agent.yaml --interval 1h --deliver telegram:123456
    # or
    praisonai schedule agent.yaml -i 1h -d telegram:123456
    ```
  </Tab>
</Tabs>

### Token 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.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

<Note>
  A CLI `--deliver` token overrides any target resolved from YAML or a recipe.
</Note>

### 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](https://github.com/MervinPraison/PraisonAI/pull/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](/docs/docs/features/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:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# crontab -e
TELEGRAM_BOT_TOKEN=123456:AAE...
TELEGRAM_HOME_CHANNEL=-100555
0 8 * * * cd /srv/praisonai && praisonai schedule tick
```

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](/docs/features/scheduler-delivery#out-of-process-delivery) for the env-var table, chat-id resolution order, and per-platform failure modes.

<Note>
  `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.
</Note>

### 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](/docs/features/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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

***

## 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:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 intact
```

`ScheduleJob.principal` round-trips through `to_dict`/`from_dict` and is omitted when `None`, so existing on-disk store files are byte-identical.

<Note>
  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.
</Note>

***

## PM2-Style Daemon Commands

### Start Scheduler

#### With a Task Prompt

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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 3
```

### List Schedulers

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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 --follow
```

### Stop Scheduler

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Graceful shutdown
praisonai schedule stop <name>

# Example
praisonai schedule stop news-bot
```

### Restart Scheduler

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Restart with same configuration
praisonai schedule restart <name>

# Example
praisonai schedule restart news-bot
```

### Delete Scheduler

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Remove from list (must be stopped first)
praisonai schedule delete <name>

# Example
praisonai schedule delete news-bot
```

### Describe Scheduler

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Direct prompt (runs in foreground)
praisonai schedule "Your task" --interval hourly

# YAML mode (runs in foreground)
praisonai schedule agents.yaml
```

**YAML Configuration Example:**

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.00
```

`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`](https://github.com/MervinPraison/PraisonAI/blob/main/src/praisonai/praisonai/tool_resolver.py) 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.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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_search
```

<Note>
  **Tool parity ([PR #3421](https://github.com/MervinPraison/PraisonAI/pull/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.
</Note>

**Run YAML in foreground:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule agents.yaml
```

Press `Ctrl+C` to stop. Shows final statistics:

```
Execution stats - Total: 12, Success: 11, Failed: 1
Total cost: $0.0056
Runtime: 3600.5s
```

## 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](/docs/tools/schedule-tools#timezones-for-at-and-cron).
* **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](https://github.com/MervinPraison/PraisonAI/pull/3527).

**Example — daily morning brief that survives restarts:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule start morning-brief "Summarise today's news in 5 bullets" \
  --interval "cron:0 9 * * *" \
  --deliver telegram:123456
```

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](https://github.com/MervinPraison/PraisonAI/pull/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](/docs/features/async-scheduler#daemon-state-persistence).

## Storage Locations

* **Schedule data:** `~/.praisonai/config.yaml` (under the `schedules` key)
* **Log files:** `~/.praisonai/logs/*.log`

<Note>
  Schedules are stored in the same `config.yaml` used by agents and server configuration. Legacy `jobs.json` data is auto-migrated on first use.
</Note>

## 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:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule "Post standup" --interval "weekdays at 9am"
praisonai schedule "Send digest"  --interval "every day at 8am"
```

See [Async Agent Scheduler → Natural language](/docs/features/async-agent-scheduler#natural-language) for the full grammar.

## Examples

### Example 1: Simple Prompt Scheduling

**Quick news check every hour:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule "Search for latest AI news and summarize top 3 stories" --interval hourly --verbose
```

**System monitoring every 15 minutes:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule "Check system health and disk space" --interval "*/15m"
```

**With budget limit:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule "Analyze market trends" \
  --interval "*/30m" \
  --max-cost 0.50 \
  --timeout 120
```

### Save Configuration

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Export scheduler config to YAML
praisonai schedule save <name> [output.yaml]

# Example
praisonai schedule save news-bot news-config.yaml
```

## Command 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 `null` in YAML to disable.
* Use `--verbose` to see detailed logs. Without it, output is clean for background running.

<Note>
  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.
</Note>

### Example 2: News Monitoring with YAML (Advanced)

**agents.yaml:**

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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: true
```

**Run:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule agents.yaml
```

### Example 2: Data Collection (Every 30 Minutes)

**agents.yaml:**

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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: false
```

**Run with override:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Override to run every 15 minutes instead
praisonai schedule agents.yaml --interval "*/15m"
```

### Example 3: With Budget and Timeout Limits

**agents.yaml:**

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 spent
```

**Run:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai schedule agents.yaml --verbose
```

**Output:**

```
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 costs
```

### Example 4: Testing with Short Interval

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Test with 10-second interval
praisonai schedule agents.yaml --interval "*/10s" --verbose
```

## Python API

For programmatic control, use the async-native Python API:

### Async Agent Scheduler (Recommended)

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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())
```

<Note>
  **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](/docs/features/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](https://github.com/MervinPraison/PraisonAI/pull/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.

  ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents import Agent
  from praisonai.scheduler import AgentScheduler

  agent = Agent(
      name="NewsChecker",
      instructions="Check the latest AI news",
  )

  # Sync scheduler now runs agents that only expose astart()
  scheduler = AgentScheduler(agent=agent, task="Summarise top 3 AI stories")
  scheduler.start("hourly", max_retries=3, run_immediately=True)
  ```
</Note>

## 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

<Note>
  **Real cost tracking ([PR #2171](https://github.com/MervinPraison/PraisonAI/pull/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](/docs/docs/cli/cost-tracking#custom-pricing)) so runs aren't silently priced at \$0.
</Note>

## 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 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.)

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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):**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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](/docs/features/scheduler-delivery#delivery-outcomes) in Scheduler Delivery.

<Note>
  **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.
</Note>

# 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:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Graceful shutdown
praisonai schedule stop <name>

# Force kill if needed
praisonai schedule delete <name>
```

### Python API

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Graceful async shutdown
await scheduler.stop()  # Waits up to 30s for current execution
```

### 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](https://github.com/MervinPraison/PraisonAI/pull/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](/docs/docs/features/async-agent-scheduler) - Python async-native scheduler API
* [Delivery Config](/docs/docs/features/delivery-config) - The `DeliveryRouter` machinery scheduled results reuse (`origin` / `all` need the full gateway)
* [Scheduled Run Policy](/docs/docs/features/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](/docs/docs/cli/planning) - Add planning to scheduled agents
* [Memory](/docs/docs/cli/memory) - Enable memory for scheduled agents
* [Tools](/docs/docs/cli/tools) - Add custom tools to agents
* [Examples](https://github.com/MervinPraison/PraisonAI/tree/main/examples/python/scheduled_agents) - Working examples
