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 codeRun recorded as
0COMPLETED — the run succeeded
non-zeroFAILED — 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:

VariableContains
AETHERFY_API_URLbase URL of the Aetherfy API
AETHERFY_SPAWN_IDthis run's id
AETHERFY_API_KEYbearer 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.

Python
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 run
Node
async 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 run

The 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.