aetherfy.yaml
What aetherfy.yaml is on Aetherfy
aetherfy.yaml is the single declarative file that describes an Aetherfy agent.
It is required — a deploy without it fails. It must sit at the root of the
archive you upload, or at the root of the build context when Aetherfy builds
from a linked GitHub repository.
Aetherfy accepts four filenames, and uses the first one it finds in this priority order:
| Priority | Filename |
|---|---|
| 1 | aetherfy.yaml |
| 2 | aetherfy.yml |
| 3 | .aetherfy.yaml |
| 4 | .aetherfy.yml |
A minimal valid file for Aetherfy is two fields:
name: hello-agent
runtime: python3.12A fuller example using most of the surface:
name: nightly-report
runtime: python3.12
type: job
description: Rolls up yesterday's events and writes a summary row.
workspace: reporting
entrypoint: main.py
memory_mb: 512
idle_timeout_minutes: 5
keep_alive: false
schedule: "0 3 * * *"
regions:
- us-east-1
github_dependencies:
- myorg/[email protected]Every field Aetherfy accepts in aetherfy.yaml
| Field | Type | Required | Default | Allowed values | Notes |
|---|---|---|---|---|---|
name | string | yes | — | any string | This is how a deploy picks its target agent. afy deploy uses --agent when you pass it and otherwise the name here; with neither it fails with Agent name not found. Use --agent flag or set 'name' in aetherfy.yaml. What name does not do is merge-patch onto an existing agent’s record — editing it retargets the deploy rather than renaming an agent, and renaming is afy rename. |
runtime | string | yes | — | python3.11, python3.12, python3.13, node20, node22, node20-ts, node22-ts, bun, dockerfile | Immutable after the agent exists. A change is rejected with RUNTIME_IMMUTABLE (HTTP 422). |
type | string | no | service | service, job | service runs a web server Aetherfy keeps reachable at the agent’s URL (it may be suspended while idle and resumed on the next request). job runs your code as a script with no server in it: Aetherfy invokes it, your program runs once and exits, and its exit code is what decides whether the run succeeded. A job never has a URL — see the task contract. Lowercased before validation. |
description | string or null | no | null | maxLength 1000 | Control characters are stripped; if nothing remains, the value becomes null. |
workspace | string or null | no | null | a non-blank string, or null to clear | Empty or whitespace-only is rejected. |
spawn | object or null | no | null | see below | Lets this agent start runs of your other agents. Off by default. See spawning. |
spawn.enabled | boolean | no | false | true, false | true requires workers: with none listed the deploy is refused with AGENT_SPAWN_WORKERS_REQUIRED (HTTP 422). |
spawn.workers | array of strings or null | no | null | agent names, or ["*"] | The agents this agent may spawn, either type. Each name must be a live agent at deploy time. "*" means any of your agents and must be the only entry. An empty list allows no spawns. |
regions | array of strings or null | no | null | us-east-1, eu-central-1, ap-southeast-1 | Not stored on the agent — consumed per deployment. When omitted, Aetherfy picks defaults bounded by your plan. |
memory_mb | integer or null | no | 256 | 256, 512, 1024, 2048, 8192 | An explicit null is rejected. Also bounded by your plan’s maximum memory. Cores follow memory and cannot be set separately — the rule is on /platform/limits. |
idle_timeout_minutes | integer or null | no | 5 | at least 1, up to your plan’s maximum | An explicit null is rejected. The per-plan maximum (Free 5, Starter 15, Performance 30, Enterprise unlimited minutes) is checked on every deploy, exactly as when an agent is created or updated through the API or dashboard, and only where the timeout takes effect — a service with keep_alive off. A job or an always-on service never sleeps, so its value is never refused; making it take effect (switching to service, or keep_alive: false) with a value above the maximum fails the deploy with PLAN_LIMIT_EXCEEDED (HTTP 403, detail.limit is max_idle_timeout_minutes) and nothing in the file is applied. See /platform/limits. |
keep_alive | boolean or null | no | false | true, false | An explicit null is rejected. Rejected on type: job — a task’s machine is suspended between runs and resumes when a run is triggered, so there is nothing for always-on to keep awake. Requires a plan that allows always-on agents. |
entrypoint | string or null | no | runtime default | a filename present in your archive | The file must exist or the build fails. Defaults are tabulated below. |
schedule | string or null | no | null | a 5-field UTC cron expression | Valid on either type, including an agent other agents spawn. See /agents/scheduled-tasks. |
github_dependencies | array of strings | no | [] | owner/repo@ref | Must fullmatch [A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[A-Za-z0-9._/-]+. Installed as the ref’s source archive, so a JavaScript package that builds itself on install is not built — see github dependencies. Private repositories require the Aetherfy GitHub App to be connected. |
The runtime field is immutable on Aetherfy
runtime is the one field an existing Aetherfy agent will not let you change.
Deploying a different runtime to an agent that already exists is rejected with
RUNTIME_IMMUTABLE and HTTP 422. To move to a different runtime, delete the
agent and create it again under the same name.
Aetherfy accepts exactly nine runtime values:
| Runtime | Family |
|---|---|
python3.11 | Python |
python3.12 | Python |
python3.13 | Python |
node20 | Node |
node22 | Node |
node20-ts | Node with TypeScript |
node22-ts | Node with TypeScript |
bun | Bun |
dockerfile | Your own container build |
python alone is not a valid runtime on Aetherfy. Documentation published
previously showed runtime: python, and that value fails the deploy. Always
write the full version, such as runtime: python3.12.
Entrypoint defaults on Aetherfy
If you omit entrypoint, Aetherfy uses the default for your runtime. The file
must exist in the code you upload, or the build fails.
| Runtime | Default entrypoint |
|---|---|
python3.11, python3.12, python3.13 | main.py |
node20, node22 | index.js |
node20-ts, node22-ts | index.ts |
bun | main.ts |
For the dockerfile runtime, your Dockerfile defines how the program starts.
Letting an Aetherfy agent spawn other agents
Spawning is off unless you turn it on. When you do, you also list the agents this one may start — by name, of either type:
name: support-desk
runtime: python3.12
type: service
spawn:
enabled: true
workers:
- ticket-classifier
- refund-processorspawn.enabled: true with no spawn.workers is refused at deploy with
AGENT_SPAWN_WORKERS_REQUIRED and HTTP 422. To fix it, list the agents this one
may spawn under spawn.workers, or write spawn.workers: ["*"] to allow any
of your agents.
A spawn of an agent that is not on the list is refused with
AGENT_WORKER_NOT_ALLOWED and HTTP 403. An empty list allows no spawns at all.
Why an Aetherfy agent lists the workers it may spawn
| What the list gives you | How |
|---|---|
| A compromised agent reaches only what it named. | The agent that spawns is usually the one facing the internet — a service taking requests from your users. If it is compromised — a bug, a prompt injected into an AI agent, a leaked key — the list is how far the damage reaches. Without it, every agent you own could be started, each run billed to you. With it, only the agents it declared. |
| A mistyped worker fails at deploy, not at runtime. | Every name in spawn.workers must be a live agent when you deploy, or the deploy is refused with WORKER_NOT_FOUND. A typo shows up in afy deploy, not as a failed spawn in production. |
| A worker cannot be deleted by accident. | Deleting an agent that another live agent lists is refused with AGENT_HAS_WORKER_DEPENDENTS (HTTP 409), naming the agents that depend on it. The check works because the dependency is declared. |
Letting an Aetherfy agent spawn any of your agents
To let an agent spawn any agent on your account, write "*" as the only entry:
name: orchestrator
runtime: python3.12
spawn:
enabled: true
workers: ["*"]"*" cannot be combined with names — ["*", "refund-processor"] is refused.
Choosing it gives up all three protections above: a compromised agent can start
any of your agents, there are no names for the deploy to check, and nothing is
protected from deletion because nothing is named. Use it when the set of workers
is genuinely open-ended, not to save listing them.
spawn: follows merge-patch like the rest of the file: a deploy that omits the
block keeps the agent’s spawn settings, and a block that omits workers keeps
the agent’s list. The refusal is judged on the agent after the deploy is
applied, so an agent deployed before this rule with spawning on and no list is
refused on its next deploy until you add one. The same rule applies to
spawn_enabled and allowed_workers on the
agent API.
Using collections from an Aetherfy agent
There is nothing to declare. aetherfy.yaml has no collection field, and an
agent is not restricted to a collection it named — it never was.
Aetherfy injects AETHERFY_WORKSPACE into an agent that declares a
workspace, and the SDKs use it as the namespace. Any collection in that
workspace is then reachable by name, with no configuration:
from aetherfy_vectors import AetherfyVectorsClient
client = AetherfyVectorsClient(workspace="auto")
client.search("animals", vector)
client.search("food", vector)
client.search("toys", vector)Collections your code creates at runtime work the same way and need no declaration either.
Aetherfy records which collections an agent actually reads and writes, so the agent’s page in the dashboard (its deployments view) lists them — including the ones created at runtime — without you configuring anything. That record is also what makes a collection an agent is using refuse to be deleted; see the collection lifecycle.
What idle_timeout_minutes actually does on Aetherfy
idle_timeout_minutes applies to service agents that are not always-on, and
it is the reason a rarely-used Aetherfy agent costs little to leave deployed.
After that many minutes without a real request, Aetherfy suspends the machine: the process is frozen in memory rather than shut down. The next request resumes it from that snapshot — a warm resume rather than a cold start: no image pull, no process launch, no re-initialisation, so connections and caches your code built at startup are still there when it wakes.
Two details decide how it behaves for you:
| Detail | Behaviour |
|---|---|
| What counts as activity | Real requests only. Aetherfy’s own GET /health checks are excluded deliberately — if they counted, no agent would ever go idle |
| Which agents are affected | service agents with keep_alive: false, and every job agent. A task is suspended between runs the same way an idle service is, and resumes when a run is triggered. keep_alive: true disables suspension entirely, and is rejected on type: job |
| An open WebSocket | Counts as activity for as long as it is held. The machine stays up for the life of the connection, and the idle window only starts once it closes |
So the first request after a quiet period is slower than the rest. The resume
adds about half a second on top of the response time your agent already has,
and occasionally several. Measured across us-east-1, eu-central-1 and
ap-southeast-1, over eighteen cycles on a healthy network path: a median of
0.5s added, seventeen of the eighteen under 1.3s, and one at 4.9s.
Occasionally the snapshot does not survive — a host is taken down for maintenance, or your machine is migrated. There is nothing to thaw, so the next request starts the process instead of resuming it. Measured the same way across the same three regions, over eight cycles on a healthy network path, that adds about three to four seconds to that first request rather than half a second. It is not a redeploy: your image is already there, and nothing about your agent changes.
The request that starts the process waits for it. Aetherfy holds requests that
arrive while your server is starting and forwards them once it is listening, for
up to 60 seconds from the start. A server that exits before it listens, or that is
still not listening after those 60 seconds, gets requests answered
502 agent_unavailable with a request_id. The response says nothing more,
because the reason can be your program’s error output and that can contain a
secret: find the request_id in your agent’s logs, next to the reason — the exit
code and the error output your program printed, or that it did not start
listening within 60 seconds. A server that starts listening after those 60
seconds serves every request from that moment; only the requests in between were
refused.
The same applies to GET /health on your agent: anyone can read whether it is
healthy, starting or unhealthy, but the error that made it unhealthy is only in
your logs and in the reason Aetherfy shows you for a degraded agent.
Both figures are what the wake adds, not the whole wait — your own request time is yours to measure and this is the increment on top of it.
One difference matters for your own code, and it applies to only one of the two. A cold boot runs your startup again — your imports, your clients, whatever your module does at load — so a heavy agent adds more than the figure above, which was measured with a minimal one. A resume does not run any of that: the process is thawed with the connections and caches it already built, which is why that figure holds whatever your agent does at startup.
If either matters for your workload, keep_alive: true keeps the machine
running — at the cost described on
/platform/billing.
A held WebSocket and a short idle timeout are not in conflict
idle_timeout_minutes: 1 and a WebSocket open for two hours coexist, and the
machine stays up for those two hours. Nothing is misconfigured and nothing
overrode your setting: an open connection is activity, so the idle window
never starts while one is held. It begins when the last connection closes.
This is worth stating plainly because the numbers look contradictory on a bill. An agent with a one-minute idle timeout can show hours of uptime in a period, and that is the correct reading rather than a billing error — Aetherfy meters the time your machine is up, and it was up, serving the connection you opened. If you want a long-lived connection that does not hold a machine, close it when idle and reconnect; the resume is warm rather than a cold start.
Merge-patch semantics in Aetherfy
Aetherfy applies your aetherfy.yaml as an RFC 7396 merge patch, not as a
full replacement. This is the single most consequential behaviour on this page:
a field you leave out is preserved, not reset to its default.
| In the deployed yaml | Effect on the Aetherfy agent |
|---|---|
| Field omitted | Preserved — the existing value is untouched |
| Field with a value | Set to that value |
Field explicitly null | Cleared (nullable fields only) |
So a configuration you deploy that omits schedule keeps whatever schedule the
agent already has. To actually remove a schedule you must write it explicitly:
name: nightly-report
runtime: python3.12
type: job
schedule: nullThree fields are non-nullable, and an explicit null on any of them is
rejected rather than treated as a clear:
| Non-nullable field | What null does |
|---|---|
memory_mb | Rejected |
idle_timeout_minutes | Rejected |
keep_alive | Rejected |
Merge-patch is also why a deploy from a linked GitHub repository does not wipe settings you made elsewhere — fields absent from the pushed file keep the values you set through the Aetherfy dashboard or API.
Aetherfy refuses unknown fields in aetherfy.yaml
Aetherfy refuses an aetherfy.yaml that names a field it does not accept, at
any depth. The whole file is refused — the valid fields beside the unknown one
are not applied either — and the message names the field, its path, and the
closest field Aetherfy does accept:
name: nightly-report
runtime: python3.12
type: job
schedul: "0 3 * * *"Config validation failed: unknown field `schedul`, did you mean `schedule`?Inside spawn: the path is dotted: spawn.workres is refused with
did you mean `spawn.workers`?. When no accepted field is close, the message
names only the unknown one. Every unknown field in the file is listed, separated
by ; , and the server lists any invalid values in the same message.
Every way of deploying gives the same answer:
| Where | What you get |
|---|---|
afy deploy | Refused before anything is uploaded: aetherfy.yaml: unknown field ..., the same sentence under the file’s name. afy diff refuses the file the same way. |
POST /api/v1/agents/{agent}/deploy | HTTP 422, DEPLOYMENT_CONFIG_PARSE_ERROR, the message above. |
| A push to a linked GitHub repository | The deployment is failed with the message above, and the commit gets a failure status carrying it. |
| A redeploy, or a rollback that has to rebuild, of a version stored with an unknown field | HTTP 422, DEPLOYMENT_CONFIG_PARSE_ERROR, The stored archive for version N no longer parses: followed by the message. |
Only field names are checked this way. A field you leave out is not an error: it keeps its current value, as merge-patch says.
To preview what a deploy would change and what it would preserve, run
afy diff in the directory holding aetherfy.yaml:
afy diffIt exits non-zero when there are changes, so it also works as a CI gate.
Archive limits Aetherfy enforces
The code you upload is subject to hard limits. Exceeding them fails the deploy.
| Limit | Value | Behaviour beyond it |
|---|---|---|
| Compressed upload size | 50 MB | HTTP 413 |
| Decompressed stream ceiling | 500 MB | Rejected |
| Archive members | 20 000 | Rejected |
| Size of the yaml member itself | 1 MB | Rejected |
Keeping uploads small is mostly a matter of exclusions. The Aetherfy CLI honours
a .afyignore file and applies built-in defaults including .git, .env,
__pycache__, node_modules, venv, .DS_Store, and *.log.
Lockfile requirements on Aetherfy
Aetherfy builds reproducibly, so some runtimes require a lockfile alongside your manifest.
| Runtime family | Condition | Required lockfile |
|---|---|---|
node20, node22, node20-ts, node22-ts | Always, with package.json | package-lock.json |
bun | When package.json declares a package | bun.lock or bun.lockb |
python3.11, python3.12, python3.13 | When a pyproject.toml is present | uv.lock |
A Python project using a plain requirements.txt does not need uv.lock.
For bun, commit whichever lockfile your bun writes. Bun 1.2 and later
default to the text bun.lock; bun.lockb is the older binary format and is
still accepted. A bun agent needs no lockfile at all when it has no
package.json, or when its package.json lists nothing under
dependencies, devDependencies, optionalDependencies,
peerDependencies or workspaces — Bun writes no lockfile for such a
project, so Aetherfy does not ask for one.
Exporting an agent’s current Aetherfy configuration
afy pull writes out the configuration Aetherfy currently holds for an
agent:
afy pull nightly-reportIt emits the declarative subset of the fields, and re-deploying what it emits is a no-op. One field is deliberately not emitted, because it is not stored on the agent as durable state:
| Not emitted | Why |
|---|---|
regions | Consumed per deployment rather than stored on the agent |
Use afy pull when you want to bring an agent that was configured
through the Aetherfy dashboard back under version control.