Aetherfy agent lifecycle API
Base URL https://agents.aetherfy.com/api/v1, bearer authentication, and the shared
error envelope are described on the Agent REST API index. {agent}
accepts a UUID or a name throughout.
Creating an Aetherfy agent
POST /api/v1/agents → 201
curl -s -X POST https://agents.aetherfy.com/api/v1/agents \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"reporter","runtime":"python3.12","memory_mb":512,"agent_type":"job"}'| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Required. 1–64 characters, unique per account |
description | string | null | null | Human-readable |
agent_type | enum | service | service or job |
runtime | enum | python3.11 | See the runtime list below |
memory_mb | integer | 256 | Must be within your plan’s maximum |
idle_timeout_minutes | integer | 5 | Within your plan’s maximum on a service with keep_alive off, the only agents it takes effect on |
keep_alive | boolean | false | Always-on. Counts as usage |
entrypoint | string | null | null | e.g. main.py, index.js |
model_name | string | null | null | Free-form |
spawn_enabled | boolean | false | Allow this agent to spawn other agents. Requires allowed_workers |
allowed_workers | string[] | [] | Names this agent may spawn, either type, or ["*"] for any of your agents ("*" alone). An empty list allows no spawns |
workspace_name | string | null | null | Workspace to place the agent in |
Runtimes: python3.11, python3.12, python3.13, node20, node22, node20-ts,
node22-ts, bun, dockerfile.
Creating an agent does not deploy it — it reserves the name and the configuration. See Deployments.
Your agent quota is not checked here. The quota counts deployed agents, and a freshly created agent is a draft, so you can keep as many drafts as you like on any plan — including building a replacement for an agent that is currently serving traffic, before you take the old one down. It is checked when you first deploy an agent, and when you restore one from the archive.
Which agents hold a slot follows from that. A deployed agent holds one, and
stopping it does not give it back: a stopped agent is paused, which keeps it
deployed. Archiving or deleting an agent releases its slot. Drafts never held one.
The per-agent caps are checked here — memory, idle timeout, always-on and the
dockerfile runtime are all validated against your plan before the record is
written, and an over-cap create is refused with PLAN_LIMIT_EXCEEDED.
Checking the quota at deploy rather than at create is not merely a deferral, and the deploy-time check is not redundant with anything: an agent created under your limit can reach its first deploy after a downgrade, or after another agent took the last slot.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_NAME_TAKEN | 409 | Another agent on this account already has that name |
WORKSPACE_NOT_FOUND | 404 | workspace_name does not resolve |
PLAN_LIMIT_EXCEEDED | 403 | Memory, idle timeout, always-on or dockerfile runtime exceeds your plan. Not agent count — that is checked at deploy |
AGENT_SPAWN_WORKERS_REQUIRED | 422 | spawn_enabled is true and allowed_workers is empty. List the agents it may spawn, or ["*"] for any — the reasons are on the aetherfy.yaml page |
SERVICE_UNAVAILABLE | 503 | Your plan could not be read, so no limit could be checked. Transient — retry |
Listing and reading Aetherfy agents
GET /api/v1/agents → 200, an array of agent objects.
GET /api/v1/agents/{agent} → 200, one agent object.
curl -s https://agents.aetherfy.com/api/v1/agents/reporter \
-H "Authorization: Bearer $AETHERFY_API_KEY"The agent object carries the creation fields above plus:
| Field | Type | Notes |
|---|---|---|
id | uuid | |
user_id | uuid | |
deployed | boolean | Whether the agent holds a built app right now |
url | string | null | Where to send a request, or null. See below |
status | enum | See the status table below |
regions | string[] | Regions the agent currently occupies |
pending_regions | string[] | Regions a change is still moving into |
regions_total / regions_ready | integer | Convergence counters |
is_degraded | boolean | Some regions are not serving |
degraded_reason | string | null | Why, when is_degraded is true |
failure_code | string | null | Why the agent is not where you asked it to be — failed, still archived after a refused restore, or running but not serving — when nothing else on the record says. Branch on this |
failure_message | string | null | The same reason as prose, for display. Wording may change between releases |
cron_schedule | string | null | The 5-field UTC cron expression, when a schedule is set |
cron_next_run_at | timestamp | null | Next scheduled fire |
cron_paused | boolean | Whether the schedule is paused |
cron_last_run_at | timestamp | null | |
cron_last_status | string | null | What the schedule last did: fired, skipped or missed. fired means a run was started, not that it succeeded |
cron_last_reason | string | null | Why the last occurrence did not fire, when it did not |
last_run | object | null | The most recent scheduled or manual run: id, trigger_source, state, created_at, error_message and release_version, as the run history lists it. Null when the agent has never run |
created_at / updated_at | timestamp | |
github | object | The agent’s GitHub link: the same object GET /api/v1/agents/{agent}/github returns, described in the deployments API. Always present; linked is false for an agent with no link |
failure_code is set only where the reason cannot be recovered from anything else,
and it can be set on any status.
Today it carries 11 values, app_lost_on_provider, image_lost_on_provider, agent_unhealthy, machine_lost_on_provider, restore_refused_agent_limit, restore_refused_usage_limit, restore_refused_payment_issue, restore_refused_code_unavailable, restore_refused_plan_settings, restore_refused_region_placement and restore_refused_other.
The first two sit on a failed agent. app_lost_on_provider means the compute
plane no longer has an app for the agent. image_lost_on_provider means the image
a service agent runs is no longer available, so nothing can start from it; a job
agent is never marked this way, because its next run rebuilds the image instead. In
both cases your secrets and the agent’s address are kept, and
afy deploy <agent> brings it back from your current code.
Two sit on a running agent that is not serving. agent_unhealthy is set when
every running machine of the agent is failing its health check, so none of them is
being sent requests; a request may wake a resting machine, which then fails the same
way if the code cannot start (see the machine health field below). failure_message
then carries the error the machine reports, and the code clears by itself as soon as
a machine passes again. If your code cannot start, fix it and afy deploy <agent>.
machine_lost_on_provider is set on a running service agent when the compute
plane no longer runs any machine of its live release: the machine was lost, or the
host it ran on is down. Nothing brings a machine back by itself, so the agent serves
nothing until afy deploy <agent> builds new machines; your secrets and the agent’s
address are kept. It clears when a deploy goes live, or by itself if the host comes
back and the machine runs again.
The seven restore_refused_* values sit on an archived agent: a restore was
accepted with a 202 and then refused when it ran (see
archiving and restoring). The agent
stays archived, with its code, secrets and settings kept. Each value names one
thing to do:
| Value | What to do |
|---|---|
restore_refused_agent_limit | Archive or delete another agent, or upgrade your plan, then restore again |
restore_refused_usage_limit | The account reached its plan usage limit. Upgrade, or ask support for a custom plan if you are already on the largest one, then restore again |
restore_refused_payment_issue | A payment has not gone through. Update the payment method in billing settings, then restore again |
restore_refused_code_unavailable | The stored code can no longer be used. afy deploy <agent> from your project directory |
restore_refused_plan_settings | Its memory, regions or always-on setting go beyond your current plan. Upgrade and restore again, or deploy with settings your plan includes |
restore_refused_region_placement | The regions it last ran in are no longer open to it (its workspace changed or was removed, or your account’s region changed). afy deploy <agent> to place it again |
restore_refused_other | Refused for a reason with no specific fix published yet. Contact support with the agent’s name |
A restore refusal clears as soon as you ask for a restore again, and whenever the
agent leaves archived by a restore, a deploy or a push. A null failure_code is
not a claim that the agent is healthy — an agent whose build did not compile has
null here and explains itself through its deployment’s error instead.
failure_code is separate from is_degraded on purpose. is_degraded means the
agent is serving and something about it is wrong; a failed agent is not serving at
all, so the two never describe the same condition. For the same reason is_degraded
is withheld, and degraded_reason is null, while agent_unhealthy explains the agent:
an agent none of whose running machines is being sent requests is not serving, and
its failure_message already carries the error the machine reports.
The cron_* fields describe a scheduled task. cron names
the expression format only.
When an Aetherfy agent has a URL
url is the address you send requests to. It is null unless all three of these
are true:
| Condition | Why |
|---|---|
| The agent is deployed | A draft has never been built. An archived agent’s app is destroyed, so its hostname resolves to nothing until you restore it |
agent_type is service | A type: job image is not a web server. It has no listener, so there is no address to publish — see the task contract |
| Aetherfy has assigned its hostname | Assigned at an agent’s first successful deploy, so an agent that has never deployed has none. An agent first deployed before Aetherfy published agent hostnames also has none until Aetherfy backfills it, which is the one case where a deployed service agent still reads null |
deployed and url answer different questions, and a type: job agent is where
the difference shows: it deploys, so deployed is true, and it serves nothing, so
url is null. Do not read url != null as “is it deployed” — read deployed.
An agent’s hostname is fixed for its lifetime. Renaming the agent does not move it, and neither does redeploying it or archiving and restoring it — so an integration pointing at the address never has to be updated. Archiving does take the agent off that address until you restore it, which is the first row above; what the fixed hostname guarantees is that it comes back on the same one.
Aetherfy agent statuses
| Status | Meaning |
|---|---|
pending | Created, nothing built yet |
building | A deployment is building |
deploying | Built, machines coming up |
running | Serving |
failed | The last deployment failed |
paused | You stopped it — see stop below |
stopped | Aetherfy stopped it because the account was suspended |
usage_paused | Aetherfy paused it because a spend limit was reached |
suspended | Account-level suspension |
archived | Torn down, configuration and code retained |
deleting / deleted | Terminal |
paused and stopped are different states with different causes, and the
distinction matters when you are diagnosing an agent that is not serving. paused is
something you did and can undo with start. stopped means the account was
suspended — settle it and Aetherfy returns the agent to running on its own. See
Billing and spend caps.
Updating an Aetherfy agent
PATCH /api/v1/agents/{agent} → 200. Every field is optional; omitted fields are
untouched. Setting workspace_name to null moves the agent out of any workspace.
curl -s -X PATCH https://agents.aetherfy.com/api/v1/agents/reporter \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"memory_mb":1024,"keep_alive":true}'Configuration changes here take effect on the next deploy. To change what is running now, deploy again.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_NOT_FOUND | 404 | |
AGENT_TOGGLE_RATE_LIMITED | 429 | This agent has used its always-on changes for the UTC day: keep_alive, or memory_mb while always-on is on, may change only a limited number of times per agent per UTC day — the number is in the agents API reference (PATCH, 429). Retrying helps only after midnight UTC |
PLAN_LIMIT_EXCEEDED | 403 | The new configuration exceeds your plan |
WORKSPACE_NOT_FOUND | 404 | The target workspace_name does not resolve |
AGENT_DEPLOYMENT_OUTSIDE_NEW_WORKSPACE_SCOPE | 400 | The agent’s live deployment sits outside the target workspace’s region set. Carries a violations array naming the deployment version, its regions and the cap |
AGENT_SPAWN_WORKERS_REQUIRED | 422 | After the update the agent would have spawn_enabled and an empty allowed_workers — whether you enabled spawning on an agent with no list, or emptied the list of one that spawns. Nothing is changed |
SOFT_CAP_EXCEEDED / DUNNING_FROZEN | 403 | The account is frozen and the update would add cost: turning keep_alive on, or raising memory_mb above the agent’s current value, or idle_timeout_minutes on what will be a service with keep_alive off. detail.field names the field, and the message states the value asked, the current value, why the account is frozen, and what is still allowed. Lowering any of them, keeping its current value, and turning keep_alive off are always allowed. Nothing is changed — see Billing |
RESOURCE_BUSY | 503 | A change to keep_alive or memory_mb waited for the agent while another operation held it — most often a deploy going live — and gave up. Nothing was changed; retry in a few seconds |
Stopping and starting an Aetherfy agent
POST /api/v1/agents/{agent}/stop → 202 {"status": "paused", "machines": "stopping", "agent_id": "…"}
POST /api/v1/agents/{agent}/start → 202 {"status": "running", "agent_id": "…", "readiness": "serving", "resume": null}
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/stop \
-H "Authorization: Bearer $AETHERFY_API_KEY"Stop moves the agent to paused — note the endpoint is named for the verb and the
resulting status is paused, not stopped. The agent keeps its slot against your
plan’s agent count, its configuration and its code. Start brings the same machines
back without rebuilding — booted afresh, so your code starts from the top and
nothing it held in memory survives the stop.
The pause is in force the moment the call returns: the status is paused, the
agent’s awake time stops counting at that instant, and a deploy or rollback against
the agent is already refused. A paused agent is still deployed, so it keeps accruing
its daily price for its memory and regions; archive it to stop that
(Billing). The machines field reports the remaining work — Aetherfy stops
the machines in the background, giving your server its shutdown grace, and the
machine states you see under GET /api/v1/agents/{agent}/status catch up afterwards
rather than immediately.
A start sent before the machines are down is accepted, and carried out once they
are — see below. Everything else already sees the agent paused. Treat the machine
states as a view that trails, not as the pause itself.
For a service agent, start answers once every machine has booted and its own
/health has answered, or 25 seconds have passed since the call arrived.
readiness says which. A job agent is resumed without starting any machine: its
machines serve only runs, so start restores its auto-start and answers running
at once, and a machine wakes when a run needs it.
readiness | Meaning |
|---|---|
serving | Every machine’s /health answered 200. Requests are served now |
starting | Your server is not listening yet. Requests sent now are held until it is |
load_failed | Your code failed to load, so requests will fail. The reason is in the agent’s logs |
unconfirmed | The machines started in time, but nothing confirmed your code is serving: at least one gave no answer from it before the budget ran out — your code’s startup, check its logs — or a machine was being stopped by an archive, a delete or a pause you made while the start was answering. A stop is never reported as load_failed |
slow_start | Starting the machines took the whole budget, so your code was not checked. The delay was on Aetherfy’s side, not in your code |
null | A job agent, which serves no requests — or an accepted start (below), which answers before anything is started |
The wait costs a resume as long as your server takes to start, up to that bound.
The agent is running either way: readiness describes your code, not the start.
Because a paused agent still holds its quota slot, you cannot pause your way under an agent limit. Archive instead.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_ALREADY_PAUSED | 409 | Already paused, with no start pending |
AGENT_NOT_PAUSED | 409 | start on an agent that was not paused |
AGENT_ALREADY_ARCHIVED | 409 | Restore it first |
AGENT_HAS_PENDING_DEPLOYMENTS | 409 | Wait for the build to finish or fail |
AGENT_NOT_PAUSEABLE_SYSTEM_STATE | 409 | Aetherfy owns the current state, e.g. usage_paused |
AGENT_OPERATION_IN_PROGRESS | 409 | A worker holds the agent, or it is being archived or deleted, or — on stop — the start you asked for earlier is being carried out right now. Transient — retry |
AGENT_NO_MACHINES | 400 | Nothing to start; deploy first |
AGENT_RESUME_FAILED | 503 | The machines could not be started: their host was unreachable. The agent is still paused — retry. A full host is not this error: that start is accepted (below) |
AGENT_OPERATION_IN_PROGRESS is the only 409 here that is safe to retry; the others
describe a state you have to change first.
Starting while the previous stop is still finishing
stop answers once the pause is decided, and the stop itself runs after it, giving your
server its shutdown grace — so a start sent right after a stop finds the stop still
being carried out. That start is accepted:
POST /api/v1/agents/{agent}/start → 202 {"status": "paused", "agent_id": "…", "readiness": null, "resume": {"state": "pending", "reason": null, "requested_at": "…"}, "stop_bound_seconds": …, "wait_bound_seconds": …}
Nothing is started yet, and nothing needs retrying. Aetherfy keeps the request and starts
the agent as soon as the stop completes — closing the dashboard or the terminal loses
nothing. Every other start answer carries "resume": null.
stop_bound_secondsis the longest the stop can still take: how long Aetherfy takes to pick it up, the stop itself and, when a stop attempt failed and is waiting to be retried, that wait too. It is 0 once the stop is done and only the start is left (resume.stateis thenqueued).wait_bound_secondsis how long to wait for the outcome:stop_bound_secondsplus 159 seconds, Aetherfy’s pickup of the start and one full attempt at it. A start that works takes seconds.
If wait_bound_seconds passes with the agent still paused and resume still
pending or queued, the start has not failed: its compute provider refused or was
slow, and Aetherfy keeps retrying it on its own schedule. Stop waiting and read the
agent later — resume becomes null with the agent running once it starts, or
dropped, reason start_failed, if every retry fails. afy start --wait exits 4 at
that point, and the dashboard says Aetherfy is still retrying.
Follow the agent on GET /api/v1/agents/{agent}. Its resume field says what became
of the start:
resume.state | Meaning |
|---|---|
pending | Accepted; the stop is still being carried out. The agent reads paused |
queued | The stop finished — or there was none, for a start accepted on a full host (below) — and Aetherfy is starting the agent. It still reads paused |
dropped | The start was not carried out; resume.reason says why |
Once the agent is started, it reads running and resume is null. afy start --wait
follows this for you, and the dashboard shows the agent as starting once its stop
finishes — or why the start was dropped. Sending start again while one is pending
changes nothing.
resume.reason, when the start was dropped:
resume.reason | Why |
|---|---|
archived | The agent was archived |
deleted | The agent was deleted |
billing_hold | A spend-limit pause or an account suspension took the agent |
agent_failed | The agent failed: its app or its image was lost |
stop_failed | Its stop never completed, so Aetherfy did not start machines whose state it could not establish |
start_failed | Its start never completed: Aetherfy gave up after its retries |
state_changed | The agent left paused some other way |
A dropped start stays on the agent until the agent next runs, or is paused again.
Starting when a machine’s host is full
A stopped machine resumes on the host it last ran on. When that host has no room left
to resume it, start is accepted the same way, at once, instead of waiting while
Aetherfy finds the machine a new host:
POST /api/v1/agents/{agent}/start → 202 {"status": "paused", "agent_id": "…", "readiness": null, "resume": {"state": "queued", "reason": null, "requested_at": "…", "cause": "host_full"}, "stop_bound_seconds": 0, "wait_bound_seconds": 413}
Aetherfy retries for about 30 seconds, then starts a new machine from the agent’s
current release on a host that has room, and destroys the old machine once the new one
is running. That is a cold start on a new machine, not a resume of the old one.
wait_bound_seconds is 413 seconds: the pickup of the start, one attempt at it, the
retries, and one new machine, which can take up to 224 seconds more for each further
machine whose host was full.
resume.cause | Why the start was accepted |
|---|---|
host_full | A machine’s host had no room to resume it, so Aetherfy starts the agent on a new machine |
resume.cause is null for a start accepted while the previous stop finishes.
Follow the agent as above. Once it reads running, its machines_recreated says how
many of its machines were replaced this way — 0 when the retry got through and the
old machine resumed where it was — and keeps saying so until the agent is paused
again. afy start --wait names it as a cold start. If a new machine cannot be started
either, Aetherfy retries the whole start on its own schedule, keeps the agent paused
with its old machine, and resume ends dropped with reason start_failed if every
retry fails.
The latest request wins:
- A
stopwhile a start is pending cancels it and answers 202 as a pause (machinesisstoppedif the stop had already finished). The pause is your own latest request, soresumethen readsnull, notdropped. If the start is already being carried out, thestopanswersAGENT_OPERATION_IN_PROGRESS; retry it once the agent isrunning. - An archive or a delete cancels it (
archived,deleted). - If the stop fails for good, the start is not carried out: Aetherfy does not start
machines whose state it could not establish. The agent stays
pausedand the start readsdropped,stop_failed. Start it again when you want it running.
Archiving and restoring an Aetherfy agent
POST /api/v1/agents/{agent}/archive → 202 {"status": "archiving", …}
POST /api/v1/agents/{agent}/restore → 202 {"status": "restoring", …}
Archiving tears the agent’s infrastructure down and releases its quota slot, keeping its configuration and stored code. It is the way to get under your agent limit without deleting anything.
Restoring rebuilds it — and re-checks quota at that moment, because other agents may have taken the slot meanwhile. An archived agent is therefore not guaranteed to come back if you have filled your plan since.
The 202 means the restore was accepted, not that it is done. When it runs it is
checked again — the agent limit, the usage limit and payment, the stored code, the
plan and the regions — and a restore refused then leaves the agent archived with
failure_code naming the reason and the fix (see the values above). A restore that
is still running has a null failure_code.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_ALREADY_ARCHIVED | 409 | |
AGENT_NOT_ARCHIVED | 409 | restore on an agent that was not archived |
AGENT_NOT_ARCHIVABLE_SYSTEM_STATE | 409 | Aetherfy owns the current state |
AGENT_HAS_PENDING_DEPLOYMENTS | 409 | |
AGENT_HAS_WORKER_DEPENDENTS | 409 | Another agent lists this one in spawn.workers. Carries dependents |
PLAN_LIMIT_EXCEEDED | 403 | On restore: no quota slot free any more |
Deleting an Aetherfy agent
DELETE /api/v1/agents/{agent} → 202 {"status": "deleting", "agent_id": "…"}
Deletion is permanent and asynchronous: the agent moves to deleting and a worker
tears down machines and stored code. It is refused while something still depends on
the agent.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_HAS_PENDING_DEPLOYMENTS | 409 | Carries pending_deployments. Wait for active or failed |
AGENT_HAS_WORKER_DEPENDENTS | 409 | Carries dependents — the agents naming this one in spawn.workers |
Deleting is not how you free a quota slot temporarily. Archive does that reversibly.
Reading live Aetherfy machine status
GET /api/v1/agents/{agent}/status → 200
curl -s https://agents.aetherfy.com/api/v1/agents/reporter/status \
-H "Authorization: Bearer $AETHERFY_API_KEY"{
"agent_id": "6f1c2b7e-0a2d-4f8e-9c31-2b0d5a7e4411",
"name": "reporter",
"status": "running",
"url": "https://reporter-k3m7x2.aetherfy.dev",
"machines": [
{
"id": "1857a3d4f21e08",
"region": "iad",
"run_id": null,
"state": "started",
"source": "live",
"health": "ok",
"last_check": "2026-08-19T09:12:44Z",
"instance_id": "01J9…"
}
]
}This route differs from GET /agents/{agent} in one important way: it queries the
machine host directly, so machines[].state is real-time rather than whatever
Aetherfy last recorded.
Read source before trusting state. It is "live" when that machine was in
the host’s response, and "cached" when Aetherfy fell back to its own last-known
value because the host read failed. A cached reading can be up to about a minute
stale. If your automation waits for a machine to reach a state, require
source == "live" — otherwise a stale reading can satisfy the wait before the state
is actually true.
health is the last health-check reading of a started machine: ok, degraded
or unhealthy. A machine in any other state reports unknown — health checks run
only while a machine is started, so a task machine suspended between runs, or an
idle service asleep, has no reading. That is a resting machine, not a problem.
A service machine whose health check fails is taken out of rotation: requests go
only to the agent’s machines whose /health answers. If every machine of the agent
fails its check, none of them is sent a request, so a request waits and is then
answered 503. The wait is the compute plane’s own routing behaviour, not a
platform setting: we measured 37 to 51 seconds. The agent’s reason says why, with
the error its machine reports, and clears once a machine passes again. A second
region gives the agent a second machine, which keeps serving while the other fails.
url is null until the agent has machines. Destroyed machines are omitted.
run_id names the run a machine is executing right now, and is null for a machine
that is not executing one: a service’s machine, or a task machine resting between
runs. It is how to tell which machine holds a run when a task agent has several.
Exporting an Aetherfy agent’s aetherfy.yaml
GET /api/v1/agents/{agent}/yaml → 200, Content-Type: application/yaml
curl -s https://agents.aetherfy.com/api/v1/agents/reporter/yaml \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-o aetherfy.yamlThe response body is YAML, not JSON. It is the declarative subset of the agent’s current configuration — the same content the dashboard’s download button produces — so it round-trips: write it into a project directory and deploy, and you get the agent back. See the aetherfy.yaml reference.
Returns 404 AGENT_NOT_FOUND for an agent being deleted.