The task contract
A task runs as a plain script. There is no framework to adopt, no handler signature to match, and no server to keep alive — Aetherfy starts a machine, executes your entrypoint, and reads the exit code. This page is the whole contract.
Entrypoint and exit code
Your entrypoint file is executed top to bottom, so a Python if __name__ == "__main__": block fires normally. When the process ends, the exit code decides the outcome:
| Exit code | Run recorded as |
|---|---|
| 0 | COMPLETED — the run succeeded |
| non-zero | FAILED — the reason is kept with the run |
An uncaught exception exits non-zero on every runtime, so a crash fails the run without any extra work from you. Running out of memory is also recorded as a failure, noted as such on the run.
Everything your task writes to stdout and stderrbecomes the run's logs — plain print / console.log is the supported logging interface. Logs are retrievable per run, so write enough to explain a failure to yourself later.
Input payload
A run started on demand can carry a JSON payload. A scheduled fire carries none — the clock has nothing to say. Fetch it from the API using the environment variables injected into every run:
| Variable | Contains |
|---|---|
| AETHERFY_API_URL | base URL of the Aetherfy API |
| AETHERFY_SPAWN_ID | this run's id |
| AETHERFY_API_KEY | bearer token scoped to this agent |
Call GET {AETHERFY_API_URL}/deployments/{AETHERFY_SPAWN_ID}/payload with Authorization: Bearer {AETHERFY_API_KEY}. Any HTTP client works — no SDK required, the standard library is enough.
import json
import os
import urllib.request
def get_payload():
"""Returns the run's input payload, or {} for a scheduled fire."""
spawn_id = os.environ["AETHERFY_SPAWN_ID"]
url = f"{os.environ['AETHERFY_API_URL']}/deployments/{spawn_id}/payload"
req = urllib.request.Request(
url,
headers={"Authorization": f"Bearer {os.environ['AETHERFY_API_KEY']}"},
)
with urllib.request.urlopen(req) as resp:
return json.load(resp).get("payload", {})
if __name__ == "__main__":
payload = get_payload()
target_date = payload.get("date") # None on a scheduled fire
print(f"building report for {target_date or 'yesterday'}")
# ... do the work; raise or sys.exit(1) to fail the runasync function getPayload() {
const spawnId = process.env.AETHERFY_SPAWN_ID;
const res = await fetch(
`${process.env.AETHERFY_API_URL}/deployments/${spawnId}/payload`,
{ headers: { Authorization: `Bearer ${process.env.AETHERFY_API_KEY}` } }
);
const body = await res.json();
return body.payload ?? {};
}
const payload = await getPayload();
const targetDate = payload.date; // undefined on a scheduled fire
console.log(`building report for ${targetDate ?? "yesterday"}`);
// ... do the work; throw or process.exit(1) to fail the runThe response shape is { "payload": { ... } }. For a scheduled fire, or a manual run started without input, it is an empty object — a 200, not an error. Write your task so the no-input case is the normal path and the payload only refines it; the same code then works both on schedule and on demand.
Design for at-most-once
A scheduled occurrence fires at most once. In a rare infrastructure failure a fire is abandoned loudly rather than attempted twice, and a failed run is not retried — the next attempt is the next scheduled occurrence.
Two consequences worth designing around:
- Make the work idempotent. Re-running a task after a failure — on the next occurrence or by hand — should converge, not double-count. Upsert rather than insert; key writes by the period they cover.
- Do not assume every occurrence ran. Runs can be skipped or missed (see running & managing). Derive the window you process from the data — "everything since the last successful watermark" — rather than assuming exactly one interval elapsed.
The 60-minute backstop
A run still executing after 60 minutes is terminated and recorded as failed. The time it ran is billed. This is a cost-safety backstop against a task that hangs — it is not a target. Split work that legitimately exceeds an hour into smaller scheduled batches.
What a run costs
Runs are metered per second of execution, exactly like any other compute on the platform — you pay for the seconds your task is actually running, not for the schedule and not for the idle time between fires. Having a schedule costs nothing by itself. See pricing for your plan's included allowance and rate.