Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agno.com/llms.txt

Use this file to discover all available pages before exploring further.

AgentOS comes with a built-in cron-style scheduler. Use it to run morning briefings, daily triage, weekly digests, hourly health checks. Advanced agentic systems like Scout also use it to set reminders and manage their own schedule. Registered schedules live in the AgentOS database (agno_schedules table) and use the same FastAPI process used by the agents.
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, and persists state.

Two ways to create schedules

PatternHow
Agent ManagedAgent has tools to create, read, update active schedules. Users ask the agent to schedule things in chat, agent uses tool calls to manage schedules.
Manually RegisteredSchedules created in code, registered at startup.

Agent Managed

Give an agent SchedulerTools and it can schedule its own work via chat:
from agno.tools.scheduler import SchedulerTools

agent = 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 Demo OS contains a Scheduler agent that does this.

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:
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="/workflows/daily-digest/runs",
        if_exists="update",                       # idempotent on restart
    )
    yield

agent_os = AgentOS(..., 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, issue triage, and repo sync.

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 aren’t strictly a scheduling feature, but they’re the most common thing a schedule fires. A workflow is a typed pipeline. Steps run in order. Parallel runs them concurrently. Loop repeats until a condition holds. Router picks one branch.
from agno.workflow import Workflow, Step, Parallel, Loop, Router, Condition

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 — pair it with Router for dynamic branches, not with Loop. Workflows are first-class AgentOS citizens: /workflows/<id>/runs POST endpoint, schedulable, traced, persisted in the same db.
PatternUse when
SequentialSteps depend on each other
ParallelSteps are independent and you want fanout
Loop with conditionQuality threshold or max iterations
Router + ConditionDynamic branching on input
Cross-modal chainingOutput of one agent is input to a different modality (text → speech, code → narration)
For worked examples, see 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 — the same path an external caller would take, including auth headers.
  3. Records the result in agno_schedule_runs (status, attempt, timings, error if any) and the underlying run in agno_sessions and agno_traces like any other run.
That means scheduled work shows up in the same UI, the same SQL queries, and the same trace tree as ad-hoc work. To see what fired in the last 24 hours:
SELECT
    s.name,
    sr.status,
    sr.triggered_at,
    (sr.completed_at - sr.triggered_at) AS duration_s
FROM agno_schedule_runs sr
JOIN 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;
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 for the full data model.

Scheduler in HA

Every replica can run the scheduler loop safely. 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; the lease is the coordination primitive. If you’d rather keep scheduler polling off your hot request path, pin it to a dedicated replica via deployment config. See Scheduler for tuning details.