> ## Documentation Index
> Fetch the complete documentation index at: https://agno-v2-codex-docs-audit-20260719-0149.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduling

> Built-in cron for triggering recurring tasks.

AgentOS comes with a built-in cron-style scheduler. Use it to run morning briefings, daily triage, weekly digests, hourly health checks. Agents can also manage their own schedules through tool calls.

Registered schedules live in the AgentOS database (`agno_schedules` table). The scheduler runs in the same FastAPI process as your agents. No separate worker.

```python theme={null}
from agno.os import AgentOS

agent_os = AgentOS(
    agents=[agent],
    db=db,
    scheduler=True,
    scheduler_poll_interval=15,    # check for due jobs every N seconds
)
```

The scheduler polls `agno_schedules` every `scheduler_poll_interval` seconds, fires due jobs, retries failures up to each schedule's `max_retries`, and persists state.

The scheduler fires a due job by calling its endpoint over HTTP, against `http://127.0.0.1:7777` by default. That matches the default `serve()` port. Set `scheduler_base_url` to match when you serve on a different host or port; otherwise schedules fire against the wrong URL.

## Two ways to create schedules

| Pattern                 | How                                                                                                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Agent Managed**       | `SchedulerTools` lets an agent create, list, inspect, delete, enable, disable, and review runs for schedules. Creating a schedule with an existing name updates it. |
| **Manually Registered** | Schedules created in code, registered at startup.                                                                                                                   |

### Agent Managed

Give an agent `SchedulerTools` and it can schedule its own work via chat:

```python theme={null}
from agno.agent import Agent
from agno.tools.scheduler import SchedulerTools

agent = Agent(
    id="my-agent",
    model="openai:gpt-5.4",
    tools=[
        SchedulerTools(
            db=db,
            default_endpoint="/agents/my-agent/runs",
            default_method="POST",
            default_timezone="UTC",
        ),
    ],
)

# In Slack: "@MyAgent post a daily digest of open PRs at 9am ET"
# The agent calls SchedulerTools.create_schedule() with a cron expr.
```

The [Scheduler Tools Agent example](/examples/agent-os/scheduler/scheduler-tools-agent) is a runnable version of this pattern.

### Manually Registered

For schedules that should always exist (the daily digest, the hourly sync, the nightly cleanup), create them in your app's lifespan via `ScheduleManager`:

```python theme={null}
from contextlib import asynccontextmanager
from agno.scheduler import ScheduleManager

@asynccontextmanager
async def lifespan(app):
    manager = ScheduleManager(db=db)
    manager.create(
        name="daily_digest",
        cron="0 9 * * 1-5",                       # weekdays 9am
        endpoint="/agents/my-agent/runs",
        payload={"message": "Create the daily digest."},
        if_exists="update",                       # idempotent on restart
    )
    yield

agent_os = AgentOS(agents=[agent], db=db, scheduler=True, lifespan=lifespan)
```

`if_exists="update"` makes the call idempotent. Re-running on restart updates the existing schedule rather than raising or duplicating. Pass `"skip"` if you want to leave manually-edited schedules alone, or `"raise"` (the default) to surface accidental name collisions. This is the pattern Coda uses for [daily digest and repo sync](/deploy/templates/coda/overview).

## Workflows for multi-step jobs

Schedules fire single endpoints. When the work is multi-step (research, then outline, then draft, then review), you reach for a workflow. Workflows are a separate primitive, but they're the most common thing a schedule fires.

A workflow is a typed pipeline whose steps run in order. Use `Parallel` for concurrent steps, `Loop` for repetition, and `Router` to select one branch.

```python theme={null}
from agno.workflow import Loop, Step, Workflow

workflow = Workflow(
    name="content_pipeline",
    steps=[
        Step(name="research", agent=researcher),
        Step(name="outline", agent=outliner),
        Loop(
            name="draft_review",
            steps=[
                Step(name="draft", agent=writer),
                Step(name="review", agent=editor),
            ],
            end_condition='last_step_content.contains("APPROVED")',
            max_iterations=3,
        ),
    ],
)
```

`Loop.end_condition` accepts a CEL expression string (as above) or a callable that takes the iteration's step outputs and returns a bool. `Condition` is a separate primitive for if/else branching inside a workflow.

<Note>
  CEL string conditions need the optional `cel-python` dependency: `pip install cel-python` or `pip install 'agno[cel]'`. Without it, the expression logs an error, evaluates to `False`, and the loop runs to `max_iterations`. The callable form has no extra dependency.
</Note>

Registered workflows get a `POST /workflows/<id>/runs` endpoint and can be scheduled. Session history is persisted when the workflow has a database. Traces are stored when AgentOS tracing is enabled.

| Pattern                  | Use when                                                                               |
| ------------------------ | -------------------------------------------------------------------------------------- |
| **Sequential**           | Steps depend on each other                                                             |
| **Parallel**             | Steps are independent and you want fanout                                              |
| **Loop with condition**  | Quality threshold or max iterations                                                    |
| **Router + Condition**   | Dynamic branching on input                                                             |
| **Cross-modal chaining** | Output of one agent is input to a different modality (text → speech, code → narration) |

For worked examples, see [Demo OS](https://github.com/agno-agi/demo-os).

## Schedule runs and observability

When a schedule fires, AgentOS:

1. Looks up the schedule in `agno_schedules` and claims it via a row-level lease.
2. Calls the configured endpoint (`POST /agents/<id>/runs` or `POST /workflows/<id>/runs`) over HTTP via `httpx.AsyncClient`. This is the same path an external caller would take, including auth headers.
3. Records the schedule attempt in `agno_schedule_runs` with status, timings, the underlying `run_id` and `session_id` when returned, and any error. The target component persists its run according to its database configuration. Traces require tracing to be enabled.

Schedule runs are queryable from `agno_schedule_runs`. When the target component persists sessions and AgentOS tracing is enabled, the linked run also appears in session and trace views. This Postgres query lists runs fired in the last 24 hours:

```sql theme={null}
SELECT
    s.name,
    sr.status,
    sr.triggered_at,
    (sr.completed_at - sr.triggered_at) AS duration_s
FROM ai.agno_schedule_runs sr
JOIN ai.agno_schedules s ON s.id = sr.schedule_id
WHERE sr.created_at > extract(epoch from NOW() - INTERVAL '24 hours')::bigint
ORDER BY sr.created_at DESC;
```

The `ai.` prefix is the schema `PostgresDb` creates its tables in by default (override with `PostgresDb(db_schema=...)`). Timestamps on schedule runs are stored as epoch seconds (BigInt). For the trace of a specific scheduled run, follow the `run_id` from `agno_schedule_runs` back to `agno_traces`. See [Observability](/features/observability) for the full data model.

## Scheduler in HA

Every replica can run the scheduler loop safely on the backends that implement the scheduler's claim methods (Postgres, SQLite, and MongoDB). Due schedules are claimed via a row-level lease (`locked_by`, `locked_at` on `agno_schedules`). The first replica to claim a due job runs it; the others skip. No leader election needed.

If you'd rather keep scheduler polling off your hot request path, pin it to a dedicated replica via deployment config. See [Scheduler](/agent-os/scheduler/overview) for tuning details.
