Skip to main content
A schedule fires an agent on a cron: each firing starts one session with a fixed first message, so a recurring job needs no scheduler of your own. The session is ordinary — same runtime, streaming, and destinations as any other.
Preview — APIs may change before general availability.
Declare schedules in the agent’s agent.toml, or manage them over the SDK, CLI, or REST. They live on the agent, not a session.

How it works

A platform tick runs every minute: it claims schedules whose next_fire_at has passed, enacts each, and advances next_fire_at to the next occurrence. Enacting starts a session on the agent’s active revision at fire time with input as the first message; everything downstream — boot, events, SSE, destinations — is the normal session path.
MechanicBehavior
Cron5 fields, 1-minute resolution (a seconds field is rejected). tz is an IANA name; omit for UTC. DST follows the cron library — a match in a skipped hour fires at the next real occurrence; a repeated hour fires once.
AttributionThe first user.message carries actor { kind: "schedule", id, display }; the session carries metadata.schedule = { id, run_id, scheduled_for }.
IdempotencyOne session per slot, keyed sch_<id>:<slot>. A tick that crashes and retries converges on the same session.
Overlapskip (default): if the previous run’s session is still queued/running, record skipped and advance. allow: fire regardless.
Catch-upAfter downtime, a schedule fires once — the run’s scheduled_for is the slot that was due — then advances past now. It never replays a backlog.
FailureFive consecutive scheduled-firing enactment failures (missing credential, no credit) → auto_paused with last_error. Session outcomes never change schedule state; neither do manual fires.
Common crons (minute hour day-of-month month day-of-week):
CronFires
*/15 * * * *every 15 minutes
0 9 * * 1-509:00 on weekdays
0 0 * * 0Sundays at midnight
0 0 1 * *first of the month

agent.toml

An agent can carry several schedules — the same behavior on different cadences and inputs — each a [[schedules]] entry keyed by a name unique to the agent.
[[schedules]]
name  = "docs-sweep"
cron  = "0 9 * * 1-5"        # weekdays 09:00
tz    = "Europe/London"      # optional; UTC if omitted
input = "Reconcile docs/ against changes since yesterday; open a draft PR if anything drifted."

[[schedules]]
name  = "dep-audit"
cron  = "0 0 * * 0"          # Sundays 00:00 UTC
input = "Audit dependencies for CVEs; file an issue per finding."
input is the entire first message and no human is in the loop, so write it as a self-contained instruction. Each deploy (git push or oc agent deploy) syncs the table: upsert by name, drop schedules no longer declared, preserve paused. Omitting the [[schedules]] table syncs nothing — existing schedules are left alone. A schedule is a binding, not behavior: it sits outside the revision digest, so editing one doesn’t cut a revision and a rollback doesn’t touch it.
For a repo-linked agent the agent.toml owns the schedules — the API and dashboard can pause/resume but reject other edits (409), same as prompt/model.

Management API

Org-key authenticated, server-side. Schedules are addressed under the agent. Create:
const schedule = await oc.agents.schedules.create("agt_...", {
  name: "morning-docs-sweep",
  cron: "0 9 * * 1-5",
  tz: "Europe/London",       // optional — UTC if omitted
  input: "Reconcile docs/ against changes since yesterday; open a draft PR if anything drifted.",
  overlap: "skip",           // skip (default) · allow
});
// → { id: "sch_...", state: "active", nextFireAt: "2026-07-07T08:00:00Z", ... }
List and fetch:
const schedules = await oc.agents.schedules.list("agt_...");
const schedule = await oc.agents.schedules.get("agt_...", "sch_...");
Update, pause, resume. PATCH accepts cron, tz, input, overlap, and paused. A cron/tz change recomputes next_fire_at from now; resume recomputes from now and resets the failure counter — the paused window never back-fires.
await oc.agents.schedules.update("agt_...", "sch_...", { cron: "0 7 * * *" });
await oc.agents.schedules.update("agt_...", "sch_...", { paused: true });   // pause
await oc.agents.schedules.update("agt_...", "sch_...", { paused: false });  // resume
Delete — removes the schedule; run history is retained.
await oc.agents.schedules.delete("agt_...", "sch_...");
Fire now — enacts immediately in any state (paused included) and doesn’t advance the cron. The test loop is create then fire. A failed manual fire returns the run with outcome: "failed" and updates last_error, but never changes the schedule’s state or failure counter.
const run = await oc.agents.schedules.fire("agt_...", "sch_...");
// → { id: "srn_...", outcome: "enacted", sessionId: "ses_..." }
Runs — newest-first; each row carries session_id to chain into GET /sessions/:id.
const { runs, nextCursor } = await oc.agents.schedules.runs("agt_...", "sch_...", { limit: 50 });

The schedule object

FieldNotes
id agent_idsch_… · the agent whose active revision each run uses
name^[a-z0-9][a-z0-9-]{0,63}$, unique per agent — the agent.toml / CLI handle
cron tz?5-field cron · IANA name, or null for UTC
inputfirst user message of every run (≤ 32 KiB)
overlapskip (default) · allow
stateactive · paused · auto_paused
next_fire_at last_fired_at?next occurrence, UTC (derived) · last fire
consecutive_failures last_error?in-a-row enactment failures (≥ 5auto_paused) · last error, cleared on success
created_at updated_attimestamps

States

StateMeaning
activeFiring on schedule.
pausedYou paused it; the cron is ignored until resume.
auto_pausedFive straight enactment failures paused it; last_error says why.

Runs

Each firing decision appends a srn_… run, so the history shows why a slot did or didn’t produce a session.
outcomeWhen
enactedA session was created — session_id is set.
skippedThe previous run was still going and overlap is skip.
failedEnactment errored; error is set. Scheduled firings count toward auto-pause; manual fires don’t.
Fields: id (srn_…), scheduled_for? (the slot; null for a manual fire), fired_at, outcome, session_id?, error?.

Limits

  • ≤ 20 schedules per agent.
  • input ≤ 32 KiB.
  • Cron floor 1 minute.
Scheduled sessions run unattended — cap them. Set agent limits turns and turn_seconds (every run inherits them). limits.tokens is not enforced yet, so don’t rely on it to bound spend.

Dashboard

The agent’s Schedules tab lists each schedule with its next fire and state, plus controls (pause/resume, test-fire, edit, delete) and per-schedule run history. The create form takes a cron — with quick presets and a plain-English gloss for common expressions — and a message; schedules created here run in UTC (a zone set via the API/CLI still displays). Repo-linked schedules are read-only except pause/resume; auto_paused shows the last error.

Example

  1. agent.toml declares [[schedules]] name = "morning-docs-sweep", cron = "0 9 * * 1-5". git push deploys the agent and the schedule goes active.
  2. Weekday 09:00 → a session starts on the active revision, first message = input, actor.kind = "schedule".
  3. The agent reconciles docs/ and opens a draft PR — an ordinary session on the dashboard and your destinations.
  4. Next 09:00, if the prior run is still running (overlap: "skip") → the firing records skipped and the schedule advances.