Skip to content
Changelog

Schedules without a cron run only when you trigger them

ContributorAshpreet BediFounder & CEO

October 19, 20261 min read

Agno's POST /schedules now accepts a body with no cron_expr, and ScheduleManager.create() accepts cron="". Either way the schedule is stored with next_run_at set to null, so the poller never claims it, and POST /schedules/{schedule_id}/trigger runs it and records the run in its history.

Think of a report someone asks for on request, or a job a button in your app should start. Earlier releases required a valid cron on every schedule.

from agno.scheduler import ScheduleManager
 
manager = ScheduleManager(db)
report = manager.create(name="adhoc-report", cron="", endpoint="/agents/reporter/runs")

ScheduleResponse.cron_expr is now optional. To make an existing schedule on demand, send "cron_expr": "" in a PATCH; leaving the field out of a PATCH keeps the current cron.

This release also fixes endpoint edits. A schedule can record the component it runs in target_type and target_id, and archiving that component disables every schedule whose target or endpoint matches it. When you repointed a schedule over PATCH /schedules/{schedule_id} or ScheduleManager.update(), earlier releases kept the old target, so archiving the agent you had moved away from would still switch the schedule off. Both paths now rewrite the target from the new endpoint.

We ran both on AgentOS with SQLite and FastAPI's TestClient, a stub model standing in for the agent's real one:

# earlier releases
POST /schedules (no cron_expr) -> 422 "cron_expr": Field required
ScheduleManager.create(cron='') -> ValueError Invalid cron expression:
PATCH /schedules/{id} -> 200: {"endpoint": "/agents/new-agent/runs", "target_type": "agent", "target_id": "old-agent"}
disable_schedules_for_target('agent', 'old-agent') -> 1 disabled
ScheduleManager.update(endpoint=...) -> endpoint=/agents/new-agent/runs target_id=old-agent
 
# this release
POST /schedules (no cron_expr) -> 201 {"cron_expr": null, "next_run_at": null}
POST /schedules/{id}/trigger -> 200 status=success
ScheduleManager.create(cron='') -> next_run_at=None
PATCH /schedules/{id} -> 200: {"endpoint": "/agents/new-agent/runs", "target_type": "agent", "target_id": "new-agent"}
disable_schedules_for_target('agent', 'old-agent') -> 0 disabled
ScheduleManager.update(endpoint=...) -> endpoint=/agents/new-agent/runs target_id=new-agent

Schedules that Studio or another control plane manages keep their targets fixed, so repoint those by replacing the schedule.

See the cookbook, and learn more about the AgentOS scheduler in the documentation.

Frequently asked questions

Leave cron_expr out of POST /schedules, or pass cron="" to ScheduleManager.create(). The schedule is saved with next_run_at set to null, the poller never picks it up, and POST /schedules/{schedule_id}/trigger runs it.

null. The field is optional now, and AgentOS reports an empty stored cron as null in every schedule response.

Send cron_expr as an empty string in PATCH /schedules/{schedule_id}. Leaving cron_expr out of the PATCH body keeps the current cron.

They record which agent, team or workflow a run endpoint points at, and archiving that component disables every schedule matching them. For a schedule you manage yourself, an endpoint change over PATCH or ScheduleManager rewrites both from the new endpoint.

Shipped around the same time