Scheduled tasks
What a scheduled task is on Aetherfy
A scheduled task on Aetherfy is an agent that Aetherfy runs for you at fixed
times. It is not a separate resource: there is nothing to create, register, or
attach. One field in aetherfy.yaml — schedule: — gives an agent a schedule,
and removing that field takes it away. The schedule is only a trigger: it does
not change what kind of agent you deployed.
Each time a schedule fires, Aetherfy starts a run, and what a run does follows
the agent’s type:
type | What each run does |
|---|---|
job | Runs your entrypoint once, as the task contract describes |
service | Sends one request to your service’s own POST /aetherfy/run route — see the run route for a service |
Scheduled runs can overlap. If a run is still going when the next occurrence is due, the next one starts anyway, on a machine of its own, up to your plan’s runs-in-flight limit. A slow or stuck task can therefore have several runs executing at once. Make the task idempotent, or have it take its own lock before work that must not happen twice — see the task contract.
An overlapping run does not start instantly: its new machine has to start first, which typically takes 15–20 seconds. A run that finds a machine resting between runs starts in about a second.
A task’s run reuses the agent’s existing image, so ordinarily there is no build
step and no deploy latency at fire time. If that image is no longer available,
the run rebuilds it from the source Aetherfy stored for the agent’s deployed
version, when that source is still stored, and then runs on the result. That
one run pays for the build; the runs after it reuse the rebuilt image. A
service’s run goes to the machine the service is already serving from, so it
never builds anything.
Runs started this way are recorded with trigger_source=cron, which is how you
tell them apart from runs you started by hand. See
/agents/runs-and-logs.
Declaring a scheduled task in Aetherfy configuration
Add schedule: to the agent’s aetherfy.yaml and deploy. A complete
configuration for a task that runs daily at 03:00 UTC:
name: nightly-report
runtime: python3.12
type: job
entrypoint: main.py
memory_mb: 512
schedule: "0 3 * * *"Quote the expression. Unquoted, a YAML parser can read some expressions as something other than a string, and Aetherfy needs the literal text.
Deploy it exactly like any other Aetherfy agent:
afy deployThe same field works on a type: service agent. Its runs are requests to a
route your service serves, which is how a long-lived service does work on a
cadence without a second agent to wake it:
name: catalog-api
runtime: python3.12
type: service
entrypoint: main.py
memory_mb: 512
schedule: "0 3 * * *"The Aetherfy schedule expression format
An Aetherfy schedule expression is a 5-field cron expression, always interpreted in UTC. It is exactly the syntax you would write for any standard cron scheduler — the format is borrowed, the feature is Aetherfy’s scheduled tasks.
┌───────────── minute
│ ┌─────────── hour
│ │ ┌───────── day of month
│ │ │ ┌─────── month
│ │ │ │ ┌───── day of week
│ │ │ │ │
* * * * *| Position | Field | Range |
|---|---|---|
| 1 | minute | 0–59 |
| 2 | hour | 0–23 |
| 3 | day of month | 1–31 |
| 4 | month | 1–12 |
| 5 | day of week | 0–6 (0 = Sunday) |
One honest caveat about how Aetherfy validates this. Only the minute field
has a hand-written grammar in Aetherfy. Fields 2–5 are handed to the underlying
parser, which accepts a broader dialect than the table above — including 7 for
Sunday and name forms for the day of week. The table is the supported core, not
the outer limit of what will parse. Write expressions that fit the table and
your schedule will behave as documented.
Schedule expressions Aetherfy rejects
Aetherfy validates the expression at deploy time, so an invalid schedule fails the deploy rather than silently never firing. Each rejection has its own message.
| Rejected input | Example | Why |
|---|---|---|
Any @-alias | @hourly | Aliases are not supported — write the 5 fields |
| 6-field syntax with seconds | 0 0 3 * * * | Exactly 5 fields are required |
| An expression that can never fire | 0 0 30 2 * | “schedule never fires (impossible date combination)” — February has no 30th |
| A stepped wrapping minute range | 50-10/15 | “unsupported minute-field syntax” |
| Anything firing more often than every 300 seconds | */2 * * * * | Below the Aetherfy minimum interval — see below |
The minimum interval on Aetherfy
The minimum interval between fires on Aetherfy is 5 minutes (300 seconds). An expression that would fire more often is rejected with:
schedule: fires more often than every 300 seconds (minimum interval).
For continuous or sub-interval work, use an always-on agent (keep_alive)
instead of a schedule.That message names the right alternative. If you need work done continuously or
faster than every five minutes, a schedule is the wrong instrument — deploy a
long-running agent with keep_alive: true and control the cadence inside your
own program. Always-on agents require a plan that permits them; see
/platform/limits.
Why Aetherfy schedules are UTC only
Every Aetherfy schedule expression is interpreted in UTC. There is no timezone field, and this is deliberate rather than an omission.
A local-time schedule is ambiguous twice a year at daylight-saving boundaries: in the spring the named local time does not exist, and in the autumn it happens twice. Rather than pick a silent policy for those two days, Aetherfy defines schedules in a timezone that has no such boundaries.
If you want a task to land at a fixed local time, convert that time to UTC yourself and accept that it will drift by an hour across a daylight-saving change, or schedule it at a UTC hour where the drift does not matter.
Examples of Aetherfy schedule expressions
| Expression | Fires |
|---|---|
0 * * * * | Every hour, on the hour |
*/15 * * * * | Every 15 minutes |
0 3 * * * | Daily at 03:00 UTC |
30 2 * * 1 | Mondays at 02:30 UTC |
15 2 * * 1-5 | Weekdays at 02:15 UTC |
Where Aetherfy allows a schedule
Two rules govern where a schedule is valid on Aetherfy.
| Rule | Detail |
|---|---|
| On either agent type | A type: job agent’s run executes its entrypoint. A type: service agent’s run is a request to its POST /aetherfy/run route. |
| One schedule per agent | To run the same code on two cadences, deploy it as two agents with different names. |
The one-schedule-per-agent rule is worth planning around. Two agents pointing at
the same repository directory, differing only in name and schedule:, is the
supported way to run one program hourly and also nightly.
What a scheduled run costs before your code starts
Between runs, a task’s machine is suspended — not destroyed, and not rebuilt. Triggering a run resumes it, and your entrypoint starts in the process that resume brings back.
This is the same suspend and resume an idle service agent uses, on the same
machine shape. A resume is fast: it thaws a process that is already running
rather than starting your machine from its image.
A scheduled run of a service agent is a request like any other the service
receives. An idle service is resumed by it, exactly as it is resumed by a
customer’s request, and an always-on service is already awake. Your server is
not started per run, so nothing below about per-run startup applies to it.
Occasionally the suspended state does not survive: a host is taken down for
maintenance, or your machine is migrated. There is nothing to resume, so the
run starts your machine from its image instead. That is slower, and it is the
same difference an idle service agent sees between a resume and a cold boot,
which is measured on /agents/aetherfy-yaml.
Aetherfy has not yet published measured figures for a task specifically, and the service figures are deliberately not repeated here: they are measured as what a resume adds over a customer’s own request path, and a task has no such path because the platform is the only thing that triggers it. Quoting them for a task would be comparing two different measurements.
Whatever the wake costs, your own startup is on top of it, and is yours to measure. Your entrypoint is a fresh process every run, so your imports, your clients, and whatever your module does at load run on every run either way.
Two things follow for planning a cadence:
| If | Then |
|---|---|
| Your task runs every few minutes | The wake is noise next to the interval, and there is nothing to tune |
| Your own startup is heavy — large models, big imports, warm caches | That cost is paid on every run, and it is usually much larger than the wake. Making the process long-lived is what fixes it |
If your startup is the expensive part and you need it paid once rather than per
run, a task is the wrong shape for it. Deploy the work as a type: service
agent that does it in its POST /aetherfy/run route, and give that agent the
schedule: each run is then a request to a process that has already paid its
startup. Note that keep_alive: true is rejected on type: job: a task’s
machine is suspended between runs and has nothing for always-on to keep awake,
so the flag would only keep the machine running and billing. Always-on is
billed for the time the machine is up; see
/platform/billing.
Changing or removing an Aetherfy schedule
Aetherfy applies aetherfy.yaml as a merge patch, which gives the schedule
field four distinct behaviours depending on what you deploy.
| What you deploy | What Aetherfy does |
|---|---|
schedule omitted | Preserves the existing schedule and does not move the next run |
schedule with the same expression | Leaves the cursor alone — the next run does not shift |
schedule with a new expression | Recomputes the next run from now |
schedule: null | Removes the schedule, removes its cursor, and clears any pause you had set |
The last row matters if you are using pause as a temporary off switch. Deploying
schedule: null does not preserve a paused state for later — it clears the
pause along with the schedule, so re-adding an expression later starts a live
schedule rather than a paused one.
To stop scheduled runs without editing configuration at all, pause instead:
afy schedule pause nightly-report
afy schedule resume nightly-reportPausing and resuming, overlap behaviour, and missed windows are covered on /agents/managing.
Building a schedule in the Aetherfy dashboard
You do not have to write the expression by hand. The Aetherfy configurator at https://app.aetherfy.com/dashboard/agents/configurator builds a valid expression from presets and previews the upcoming runs in both UTC and your local time, which is the fastest way to confirm that a UTC expression lands where you expect it to locally. The builder is offered for either agent type.
What to read next about Aetherfy scheduled tasks
| Page | What it covers |
|---|---|
| /agents/task-contract | How your code runs and exits, how it reads input, the at-most-once guarantee, and the run route a service answers |
| /agents/managing | Running on demand, pausing and resuming, overlaps, missed windows |
| /agents/runs-and-logs | Run history, run states, and log retrieval |
| /agents/aetherfy-yaml | Every configuration field and the merge-patch rules |