§03 · Building agents

Schedules

Wake an agent on a cron expression, a plain-English phrase, or once at a fixed time. The operator owns the clock; the control plane decides whether each due fire is allowed to launch.

A scheduled agent runs without anyone clicking anything. The in-cluster operator holds the timer and launches the run pod; Exo is asked, at each due fire, whether that run may proceed. That split matters: pausing or deleting an agent in the dashboard stops its schedule immediately, even if the cluster is still holding a stale copy of the resource.

Set a schedule

Open an agent and edit its Schedule. You give it exactly one of three things — a phrase, a cron expression, or a single timestamp — plus an optional timezone. Turning the schedule off clears it; the agent stays runnable on demand.

Phrases

A phrase is translated to cron server-side. The vocabulary is deliberately small: a phrase either maps cleanly or the edit is rejected with examples, because a schedule that fires at the wrong hour is worse than an error.

PhraseCron
every minute* * * * *
every 15 minutes*/15 * * * *
hourly0 * * * *
daily at 9am0 9 * * *
weekdays at 07:3030 7 * * 1-5
every monday at 6pm0 18 * * 1
weekly0 0 * * 1
monthly at noon0 12 1 * *

Also accepted: midnight, noon, every weekend, day names in short or long form (every tue, fridays), and a bare clock — 9am or 17:45 — which reads as “daily at that time”.

Cron & one-shots

  • Cron. Standard five-field expressions and @-descriptors (@daily, @hourly). A CRON_TZ= or TZ= prefix is accepted; so is setting the timezone field, which applies to both cron and phrase schedules and defaults to UTC.
  • One-shot. Give an RFC 3339 timestamp instead and the agent fires exactly once at that moment — the right shape for a scheduled migration or a follow-up check.

In YAML

Schedules live on the Agent resource as spec.wakeup, so a GitOps-managed agent carries its own timer:

agent.yaml· yaml
1spec:
2 wakeup:
3 enabled: true
4 mode: schedule # interval | schedule | oneShot
5 schedule: "0 9 * * 1-5"
6 timezone: Europe/Amsterdam
7 jitter: 30s
8 concurrencyPolicy: Forbid # Allow | Forbid | Replace
9 activeDeadlineSeconds: 3600
10 backoffLimit: 2

concurrencyPolicy decides what happens when a fire comes due while the previous run is still going: run anyway, skip, or replace. activeDeadlineSeconds caps how long one run may take, and jitter spreads a fleet of agents that would otherwise all wake on the same minute.

Every fire is authorized

When a schedule comes due, the operator asks the control plane for permission before it launches anything. Exo answers from its own projection of the agent, not from the cluster: an agent that has been deleted, paused, or had running disabled is denied even if the operator still holds an older copy of the resource. If no decision arrives, the operator fails closed and skips the fire.

What gets recorded

Each decision is written to the platform events log as agent.schedule.fire.allowed or agent.schedule.fire.denied, with the agent, namespace, deployment, the scheduled time, the trigger, and the reason for a denial. The run itself shows up in Sessions like any other run, so you can replay what a 3 a.m. fire actually did.