Skip to Content
Agent computeThe task contract
Raw

The task contract

How Aetherfy runs your code

A task on Aetherfy runs as a plain script. There is no framework to import, no handler function to export, and no server to start. Aetherfy starts a machine, executes your entrypoint file, and reads the exit code when the process ends.

Your entrypoint is executed top to bottom, exactly as if you had run it yourself. A Python if __name__ == "__main__": block fires normally, and a Node file’s top-level statements run normally.

This applies to type: job agents — the run-once type. Long-lived service agents on Aetherfy serve HTTP instead and are not covered by this page.

Exit codes and how Aetherfy records the outcome

Aetherfy decides whether a run succeeded from the process exit code alone.

Exit codeRun recorded as
0, or an unknown or unparseable codecompleted
Any known non-zero codefailed — the reason is kept with the run

An uncaught exception exits non-zero on every runtime Aetherfy supports, so a crash fails the run without you writing any error handling. You do not need to call sys.exit(1) or process.exit(1) yourself to mark a failure, though doing so is fine.

Some failures carry extra detail on the run record:

FailureHow Aetherfy records it
Out of memoryA failure, annotated (oom_killed) in the run’s error message
Host or infrastructure failure”run machine entered Fly state ‘failed’ (host/infra failure)”

Signal deaths map to the conventional 128+N codes:

SignalExit code
SIGKILL, including an out-of-memory kill137
SIGTERM143

Shutdown signals on Aetherfy tasks

When Aetherfy needs to stop a task, it forwards SIGTERM to your process and then allows a 10-second grace period before SIGKILL. Use that window to flush a buffer or close a connection; do not use it for real work.

The longer 120-second shutdown contract you may see referenced elsewhere applies to service agents on Aetherfy, not to tasks. For a task, plan on 10 seconds.

Logging from an Aetherfy task

Everything your program writes to stdout and stderr becomes the run’s logs. Plain print in Python and console.log in Node are the supported logging interface on Aetherfy — there is no logging SDK to install and no special sink to configure.

print("starting nightly rollup", flush=True)
console.log('starting nightly rollup');

Aetherfy tags each record with the stream it came from (stdout, stderr, or system), so you can filter by stream when reading logs back. Retrieval flags, retention, and the per-line caps are on /agents/runs-and-logs.

Reading the input payload from Aetherfy

Input on Aetherfy is pulled, not pushed. Your task is not handed an argument or a request body; it fetches its payload over HTTP using credentials Aetherfy puts in the environment.

Every run receives these three variables:

VariableContains
AETHERFY_API_URLBase URL of the Aetherfy API
AETHERFY_SPAWN_IDThis run’s id
AETHERFY_API_KEYA bearer token scoped to this agent

Fetch the payload with:

GET {AETHERFY_API_URL}/deployments/{AETHERFY_SPAWN_ID}/payload Authorization: Bearer {AETHERFY_API_KEY}

The response shape is:

{"payload": {}}

The empty case is the normal case. For a scheduled fire, or a manual run started without input, payload is an empty object and the status is 200, not an error. Write the task so that no input is the path it takes most often, and treat any input as an optional override.

A run started by a parent agent carries its payload through the same endpoint, so one implementation covers scheduled fires, manual runs, and spawned runs.

Complete Python example

Standard library only — no dependencies to install:

"""An Aetherfy task: pulls its input payload, then does its work.""" import json import os import urllib.request def fetch_payload(): """Return this run's input payload, or {} when the run was given none.""" api_url = os.environ["AETHERFY_API_URL"] spawn_id = os.environ["AETHERFY_SPAWN_ID"] api_key = os.environ["AETHERFY_API_KEY"] request = urllib.request.Request( f"{api_url}/deployments/{spawn_id}/payload", headers={ "Authorization": f"Bearer {api_key}", # Always set an explicit User-Agent. Python's urllib otherwise sends # "Python-urllib/3.x", a signature many CDNs' bot protection blocks # outright — producing a 403 that reads like an auth failure. "User-Agent": "nightly-report/1.0", }, ) with urllib.request.urlopen(request, timeout=30) as response: body = json.load(response) return body.get("payload") or {} def main(): payload = fetch_payload() # A scheduled fire sends no input, so this is the normal path. target_date = payload.get("date") if target_date is None: print("no input payload: processing the default window", flush=True) else: print(f"input payload requested date {target_date}", flush=True) print("done", flush=True) if __name__ == "__main__": main()

Complete Node example

Uses the built-in fetch, available on the node20 and node22 runtimes:

/** An Aetherfy task: pulls its input payload, then does its work. */ async function fetchPayload() { const apiUrl = process.env.AETHERFY_API_URL; const spawnId = process.env.AETHERFY_SPAWN_ID; const apiKey = process.env.AETHERFY_API_KEY; const response = await fetch(`${apiUrl}/deployments/${spawnId}/payload`, { headers: { Authorization: `Bearer ${apiKey}`, 'User-Agent': 'nightly-report/1.0', }, }); if (!response.ok) { throw new Error(`payload fetch failed with status ${response.status}`); } const body = await response.json(); return body.payload ?? {}; } async function main() { const payload = await fetchPayload(); // A scheduled fire sends no input, so this is the normal path. const targetDate = payload.date; if (targetDate === undefined) { console.log('no input payload: processing the default window'); } else { console.log(`input payload requested date ${targetDate}`); } console.log('done'); } main().catch((error) => { console.error(error); process.exit(1); });

Environment variables Aetherfy provides

Beyond the three payload variables above, every Aetherfy agent machine carries:

VariableContains
AETHERFY_AGENT_IDThe agent’s id
AETHERFY_AGENT_NAMEThe agent’s name
AETHERFY_REGIONThe region this machine is running in
AETHERFY_WORKSPACEThe agent’s workspace, if it has one
AETHERFY_VECTORS_URLBase URL for the Aetherfy vector database
AETHERFY_SPAWN_URLEndpoint for spawning worker runs
AETHERFY_DEPLOYMENT_IDThe deployment this machine is running

The entire AETHERFY_ prefix is reserved on Aetherfy — you cannot set a secret that would shadow one of these. See /agents/secrets.

At-most-once execution on Aetherfy

A scheduled occurrence on Aetherfy fires at most once. A failed run is not retried; the next attempt is simply the next scheduled occurrence. Aetherfy has no automatic retry, no backoff, and no dead-letter queue for runs.

Two consequences are worth designing around explicitly.

ConsequenceWhat to do about it
A run can execute more than the zero times you assume, and its effects persistMake the work idempotent — upsert rather than insert, and key writes by the period they cover so a re-run overwrites rather than duplicates
An occurrence can fail to happen at allDo not assume every occurrence ran. Derive the window you process from your own data watermark (“everything since the last row I wrote”) rather than from the clock (“the last 24 hours”)

Written that way, a task that misses a night catches up automatically on the next fire, because the window it computes is wider. Written the other way, the missing night stays missing forever.

If you need every single occurrence processed with a guarantee, a schedule is the wrong tool on Aetherfy — put the work in a durable queue and let a task drain it.

The 60-minute backstop on Aetherfy

A run still executing after 60 minutes is terminated by Aetherfy and recorded as failed. The time it ran is billed.

This is a cost-safety backstop, not a target, and it is not configurable. It exists so a task stuck in a loop cannot bill indefinitely.

If your work legitimately exceeds an hour, split it into smaller scheduled batches — for example, process one hour of data per fire on an hourly schedule instead of a day of data per fire on a daily one. Each fire then finishes well inside the backstop, and the watermark pattern above makes the batches self-correcting.

Last updated on