Deploy an agent to Aetherfy
This tutorial takes you from an empty directory to a deployed Aetherfy agent
that answers HTTP requests, then shows you its logs. It uses the afy CLI
throughout and asks you to make no design decisions along the way.
You need the afy CLI installed before you start. Installation instructions are
on /cli.
Get an Aetherfy API key
Create an API key at
https://app.aetherfy.com/dashboard/settings/api-keys .
Keys look like afy_live_xxxxx. Copy it when it is shown.
The Aetherfy CLI reads your key from its stored login, and it also honours the
AETHERFY_API_KEY environment variable if one is set.
Sign in to Aetherfy with afy login
Run afy login and paste the key when prompted:
afy loginFor CI or any non-interactive shell, pass the key directly:
afy login --api-key afy_live_xxxxxConfirm the CLI is talking to your Aetherfy account:
afy whoamiCreate the agent’s files for Aetherfy
Make a directory and add two files. This example is a service agent — a
long-lived process that answers HTTP requests.
mkdir hello-agent
cd hello-agentaetherfy.yaml — the configuration Aetherfy reads. It must sit at the root of
the directory you deploy:
name: hello-agent
runtime: python3.12
type: service
memory_mb: 256Write the runtime version in full. runtime: python is not valid on
Aetherfy and fails the deploy; the Python values Aetherfy accepts are
python3.11, python3.12, and python3.13.
main.py — the entrypoint. main.py is the default entrypoint for every
python3.* runtime on Aetherfy, so you do not need to declare it.
A service agent on a Python runtime exports a FastAPI application in a
variable named app, and does not start a server itself. Aetherfy runs the
server for you: it imports your entrypoint, serves the app it finds there on
port 8080, and answers its own GET /health. This is the one contract the
tutorial asks you to follow exactly — the next section explains what happens if
you do not.
"""A minimal Aetherfy service agent: a FastAPI app exported as `app`."""
import os
from fastapi import FastAPI
# Aetherfy imports this module and serves THIS object. The name must be `app`.
app = FastAPI()
# Runs at import time, when Aetherfy starts the agent. Anything printed to
# stdout becomes this agent's logs on Aetherfy.
print("hello-agent starting", flush=True)
@app.get("/")
def index():
return {
"message": "hello from Aetherfy",
"agent": os.environ.get("AETHERFY_AGENT_NAME", "unknown"),
"region": os.environ.get("AETHERFY_REGION", "unknown"),
}
@app.post("/echo")
def echo(body: dict):
"""Reads a JSON request body and returns it. Aetherfy adds no routing of
its own — every route you declare on `app` is served as you wrote it."""
return {"you_sent": body}Both routes are yours. Aetherfy serves whatever you declare on app and adds
only GET /health, so the request shapes, paths and status codes are the
framework’s business and not Aetherfy’s — reach for FastAPI’s or Express’s own
documentation when you need path parameters, validation or custom responses.
There is no second file to write. Aetherfy installs FastAPI and uvicorn into
every Python service image itself, so this example needs no
requirements.txt at all. Add one when your agent has dependencies of its own;
Aetherfy installs from it when it is present.
The Aetherfy service contract, and what breaks without it
The export above is not a style preference. Aetherfy imports your entrypoint as a module and looks for one specific name:
| Runtime family | What Aetherfy imports | What you export |
|---|---|---|
python3.11, python3.12, python3.13 | your entrypoint module | app — a FastAPI application |
node20, node22, node20-ts, node22-ts | your entrypoint module | app — an Express application, via module.exports = { app } or a default export |
bun | your entrypoint module | app — an Express application |
dockerfile | nothing — you own the container | see /agents/dockerfile |
Three consequences follow, and the third is the one that costs an afternoon:
- Do not start a server yourself. Aetherfy runs uvicorn (or Node) against
your
app. Code guarded byif __name__ == "__main__":never executes, because Aetherfy imports the module rather than running it as a script. - Do not implement
GET /health. Aetherfy answers that path itself, in front of your app, so a/healthroute of your own is never reached. - A missing
appexport fails the deploy, and says so. Aetherfy checks for the export before building, and checks again at startup: an entry point that does not load is never reported healthy, so the deploy stops and its reason carries your interpreter’s own error message. Read the deploy’s reason first — it names the file and what went wrong with it.
This contract applies to service agents only. A job agent is executed as a
plain script, exports nothing, and is covered by
/agents/task-contract.
Letting afy init write the configuration instead
afy init inspects a directory, detects the project type, and generates
aetherfy.yaml for you. Every prompt it asks has a matching flag — --name,
--runtime, --entrypoint, --type, --region, --memory, --keep-alive,
--workspace, and --schedule. Two of those are boolean toggles rather than
value flags: --keep-alive and --workspace answer yes/no prompts and take no
argument. -y / --yes accepts all defaults, which makes it usable in CI:
afy init --name hello-agent --runtime python3.12 --type service --yes-f / --force overwrites an existing aetherfy.yaml. Note that -y does not
imply --force, so afy init -y will not clobber a file you already have.
Deploy the agent to Aetherfy
From inside the directory, run:
afy deployafy deploy uploads the current directory, builds an image, launches machines,
waits for completion, and streams status as it goes. It prompts once to confirm
cost before it starts.
The name: field in aetherfy.yaml is what this deploy targets. With no
--agent flag, Aetherfy’s CLI reads name from the file — hello-agent here —
and that is the agent the deploy goes to, and the name it is created under if
you consent to the prompt below. If you pass neither, the command stops with
Agent name not found. Use --agent flag or set 'name' in aetherfy.yaml.
Changing name later retargets the deploy at a different agent rather than
renaming this one; renaming is afy rename.
The first deploy offers to create the agent
Your code deploys to an agent: a named record on Aetherfy that owns the
deployments, the machines and the logs. hello-agent has no such record yet, so
this first deploy comes back reporting it missing and asks before going further:
Agent 'hello-agent' doesn't exist. Create it as service/python3.12 (from aetherfy.yaml)? [y/N]Answer y and the CLI creates the agent, then finishes the same deploy into it.
The service and python3.12 in the question are read from the aetherfy.yaml
you just wrote — neither is guessed. The question comes before the cost
confirmation, because the agent has to exist before Aetherfy can price it.
Creating is never automatic. The prompt defaults to No, so pressing Enter declines it and the deploy ends the way it always has, with nothing created on Aetherfy and nothing deployed:
Error: Deployment failed: [404] Agent 'hello-agent' not found (AGENT_NOT_FOUND)Anywhere without a terminal — CI, a script, a pipe — the CLI does not ask at all,
and --create is the only way to consent. A pipeline deploying a brand-new agent
passes it alongside --yes, which answers the separate cost prompt:
afy deploy --create --yesThose two flags are separate consents and --yes never implies --create.
/cli/deploy has the complete rules, including what --yes does to
this question at a terminal and why a UUID target is never created.
You can also create the agent up front, once, and leave --create out of the
deploy entirely — see /cli/agents for its flags:
afy create hello-agent --runtime python3.12 --type serviceUseful flags on the Aetherfy deploy command:
| Flag | Effect |
|---|---|
--detach / -d | Return as soon as the upload finishes instead of waiting |
--agent / -a | Deploy to a named agent, overriding the name in aetherfy.yaml |
--create | Create the agent from aetherfy.yaml’s type and runtime if it does not exist. Required to create without a terminal |
--yes / -y | Skip the cost confirmation prompt |
Aetherfy does not upload everything in the directory. Files matching
.afyignore are excluded, along with built-in defaults including .git,
.env, __pycache__, node_modules, venv, .DS_Store, and *.log.
Confirm the Aetherfy agent is running
When the deploy finishes, the deployment reaches the active state and the
agent’s status becomes running. Check it from the CLI:
afy listYou can see the same thing in the dashboard at https://app.aetherfy.com/dashboard/agents .
If the deploy failed instead, the deployment ends in the failed state and the
error is recorded against it. List the history with afy deployments hello-agent
to read the failure, and see /agents/runs-and-logs for
what each state means.
Call the Aetherfy agent
A deployed service agent has its own address, shown by afy list and
on the agent in the dashboard:
curl -s https://hello-agent-k3m7x2.aetherfy.dev/{"message": "hello from Aetherfy", "agent": "hello-agent", "region": "iad"}The POST /echo route from main.py is served exactly as written:
curl -s https://hello-agent-k3m7x2.aetherfy.dev/echo \
-H "Content-Type: application/json" \
-d '{"hello": "world"}'{"you_sent": {"hello": "world"}}No API key. Your agent’s address is public and serves whoever calls it — the key authenticates you to Aetherfy, not your callers to your agent. If your agent needs to authenticate its callers, that is a route you write.
The address is fixed at your first deploy
The six characters after the name are drawn at random the first time the agent
deploys, and the address never changes afterwards — including when you
rename the agent. An agent renamed hello-agent to greeter keeps serving
at hello-agent-k3m7x2.aetherfy.dev.
That is deliberate: a rename that moved the address would break every integration, link and webhook already pointing at it. The practical rule is rename before your first deploy and the address follows the new name; rename after and it stays as it was.
The name: in your aetherfy.yaml does move, and Aetherfy’s CLI moves it for
you: run afy rename from the project directory and it rewrites that one key,
because afy deploy resolves its target from it. Run the rename from somewhere
else and nothing on disk is touched — the command then tells you which file to
update by hand. Full behaviour is on /cli/agents.
There is no way to issue a second address for the same agent, and there will
not be one — the address is derived from the agent rather than looked up, so
one agent has exactly one address. job agents have none at all: they run to
completion and exit, so nothing is listening.
Read the Aetherfy agent’s logs
Everything your program writes to stdout and stderr becomes the agent’s logs on
Aetherfy. The startup line from main.py above appears here:
afy logs hello-agentFollow the logs as they arrive:
afy logs hello-agent --followShow more history, or narrow to a time window and a severity:
afy logs hello-agent --tail 200 --since 1h --level ERROR,WARNAetherfy retains logs for 7 days. The full flag list, the per-line and volume caps, and the dropped-line marker are documented on /agents/runs-and-logs.
Preview a change before deploying it to Aetherfy
Aetherfy applies aetherfy.yaml as a merge patch, so fields you leave out keep
their existing values rather than resetting. Before a deploy you are unsure
about, preview exactly what would change and what would be preserved:
afy diffafy diff exits non-zero when there are changes, which makes it usable
as a CI gate that fails a pipeline on unintended configuration drift.
What to do next with Aetherfy
| Next step | Page |
|---|---|
| Every configuration field and the merge-patch rules | /agents/aetherfy-yaml |
| Turn this into a run-once task on a schedule | /agents/scheduled-tasks |
| How a task reads input, exits, and is retried (it is not) | /agents/task-contract |
| Give the agent API keys and other secrets | /agents/secrets |
| Deploy automatically on every push | /agents/github |
| The rest of the Aetherfy CLI surface | /cli/agents |