Skip to Content
Agent computeREST: agent lifecycle
Raw

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"}'
FieldTypeDefaultNotes
namestring—Required. 1–64 characters, unique per account
descriptionstring | nullnullHuman-readable
agent_typeenumserviceservice or job
runtimeenumpython3.11See the runtime list below
memory_mbinteger256Must be within your plan’s maximum
idle_timeout_minutesinteger5Within your plan’s maximum on a service with keep_alive off, the only agents it takes effect on
keep_alivebooleanfalseAlways-on. Counts as usage
entrypointstring | nullnulle.g. main.py, index.js
model_namestring | nullnullFree-form
spawn_enabledbooleanfalseAllow this agent to spawn other agents. Requires allowed_workers
allowed_workersstring[][]Names this agent may spawn, either type, or ["*"] for any of your agents ("*" alone). An empty list allows no spawns
workspace_namestring | nullnullWorkspace 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.

CodeHTTPMeaning
AGENT_NAME_TAKEN409Another agent on this account already has that name
WORKSPACE_NOT_FOUND404workspace_name does not resolve
PLAN_LIMIT_EXCEEDED403Memory, idle timeout, always-on or dockerfile runtime exceeds your plan. Not agent count — that is checked at deploy
AGENT_SPAWN_WORKERS_REQUIRED422spawn_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_UNAVAILABLE503Your 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:

FieldTypeNotes
iduuid
user_iduuid
deployedbooleanWhether the agent holds a built app right now
urlstring | nullWhere to send a request, or null. See below
statusenumSee the status table below
regionsstring[]Regions the agent currently occupies
pending_regionsstring[]Regions a change is still moving into
regions_total / regions_readyintegerConvergence counters
is_degradedbooleanSome regions are not serving
degraded_reasonstring | nullWhy, when is_degraded is true
failure_codestring | nullWhy 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_messagestring | nullThe same reason as prose, for display. Wording may change between releases
cron_schedulestring | nullThe 5-field UTC cron expression, when a schedule is set
cron_next_run_attimestamp | nullNext scheduled fire
cron_pausedbooleanWhether the schedule is paused
cron_last_run_attimestamp | null
cron_last_statusstring | nullWhat the schedule last did: fired, skipped or missed. fired means a run was started, not that it succeeded
cron_last_reasonstring | nullWhy the last occurrence did not fire, when it did not
last_runobject | nullThe 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_attimestamp
githubobjectThe 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:

ValueWhat to do
restore_refused_agent_limitArchive or delete another agent, or upgrade your plan, then restore again
restore_refused_usage_limitThe 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_issueA payment has not gone through. Update the payment method in billing settings, then restore again
restore_refused_code_unavailableThe stored code can no longer be used. afy deploy <agent> from your project directory
restore_refused_plan_settingsIts 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_placementThe 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_otherRefused 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:

ConditionWhy
The agent is deployedA 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 serviceA 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 hostnameAssigned 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

StatusMeaning
pendingCreated, nothing built yet
buildingA deployment is building
deployingBuilt, machines coming up
runningServing
failedThe last deployment failed
pausedYou stopped it — see stop below
stoppedAetherfy stopped it because the account was suspended
usage_pausedAetherfy paused it because a spend limit was reached
suspendedAccount-level suspension
archivedTorn down, configuration and code retained
deleting / deletedTerminal

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.

CodeHTTPMeaning
AGENT_NOT_FOUND404
AGENT_TOGGLE_RATE_LIMITED429This 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_EXCEEDED403The new configuration exceeds your plan
WORKSPACE_NOT_FOUND404The target workspace_name does not resolve
AGENT_DEPLOYMENT_OUTSIDE_NEW_WORKSPACE_SCOPE400The 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_REQUIRED422After 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_FROZEN403The 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_BUSY503A 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.

readinessMeaning
servingEvery machine’s /health answered 200. Requests are served now
startingYour server is not listening yet. Requests sent now are held until it is
load_failedYour code failed to load, so requests will fail. The reason is in the agent’s logs
unconfirmedThe 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_startStarting the machines took the whole budget, so your code was not checked. The delay was on Aetherfy’s side, not in your code
nullA 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.

CodeHTTPMeaning
AGENT_ALREADY_PAUSED409Already paused, with no start pending
AGENT_NOT_PAUSED409start on an agent that was not paused
AGENT_ALREADY_ARCHIVED409Restore it first
AGENT_HAS_PENDING_DEPLOYMENTS409Wait for the build to finish or fail
AGENT_NOT_PAUSEABLE_SYSTEM_STATE409Aetherfy owns the current state, e.g. usage_paused
AGENT_OPERATION_IN_PROGRESS409A 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_MACHINES400Nothing to start; deploy first
AGENT_RESUME_FAILED503The 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_seconds is 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.state is then queued).
  • wait_bound_seconds is how long to wait for the outcome: stop_bound_seconds plus 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.stateMeaning
pendingAccepted; the stop is still being carried out. The agent reads paused
queuedThe 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
droppedThe 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.reasonWhy
archivedThe agent was archived
deletedThe agent was deleted
billing_holdA spend-limit pause or an account suspension took the agent
agent_failedThe agent failed: its app or its image was lost
stop_failedIts stop never completed, so Aetherfy did not start machines whose state it could not establish
start_failedIts start never completed: Aetherfy gave up after its retries
state_changedThe 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.causeWhy the start was accepted
host_fullA 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 stop while a start is pending cancels it and answers 202 as a pause (machines is stopped if the stop had already finished). The pause is your own latest request, so resume then reads null, not dropped. If the start is already being carried out, the stop answers AGENT_OPERATION_IN_PROGRESS; retry it once the agent is running.
  • 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 paused and the start reads dropped, 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.

CodeHTTPMeaning
AGENT_ALREADY_ARCHIVED409
AGENT_NOT_ARCHIVED409restore on an agent that was not archived
AGENT_NOT_ARCHIVABLE_SYSTEM_STATE409Aetherfy owns the current state
AGENT_HAS_PENDING_DEPLOYMENTS409
AGENT_HAS_WORKER_DEPENDENTS409Another agent lists this one in spawn.workers. Carries dependents
PLAN_LIMIT_EXCEEDED403On 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.

CodeHTTPMeaning
AGENT_HAS_PENDING_DEPLOYMENTS409Carries pending_deployments. Wait for active or failed
AGENT_HAS_WORKER_DEPENDENTS409Carries 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.yaml

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

Last updated on