Aetherfy runs, schedules and spawns 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 an agent UUID or its name throughout.
Running an Aetherfy agent on demand
POST /api/v1/agents/{agent}/run → 202
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/run \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"payload":{"date":"2026-08-19"}}'The body is optional; {"payload": { … }} passes input the agent reads back through
the payload route. Omit it entirely to run with no input.
{
"deployment_id": "3c9a5f10-7b2e-4d81-a6f4-19e0c8b7d224",
"version": 12,
"job_id": "8e41b0c7-2d15-4a93-b7e2-5f0c6a91d338"
}Both agent types can be run this way. A type: job agent runs its entrypoint; a
type: service agent is sent one request to its POST /aetherfy/run route — see
/agents/task-contract. The
agent must already be deployed; run does not build.
A manual run and a scheduled task occurrence produce the
same kind of ephemeral run, and both appear in the history below. A manual run does
not touch the schedule: it does not move cron_next_run_at, and it does not
count as that schedule having fired.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_NOT_DEPLOYED | 422 | Deploy the agent first |
AGENT_RUN_INELIGIBLE_STATE | 409 | The status does not allow a run. Carries current_state |
AGENT_RUN_CONCURRENCY_LIMIT_EXCEEDED | 429 | The account’s runs-in-flight limit is full. Same code and detail as a spawn’s, below |
SOFT_CAP_EXCEEDED | 403 | A spend limit was reached; new runs are frozen |
A run of an agent that already has a run in flight is not refused: a type: job
agent whose machine is busy gets a machine of its own for the new run, exactly as a
spawn does, and a type: service agent serves it as another request.
AGENT_RUN_INELIGIBLE_STATE names what to do in its message: a paused agent needs
starting, a usage_paused one needs the spend limit raised, an archived one needs
restoring, and a stopped one belongs to a suspended account — settle it and the
agent resumes on its own. The same rule holds for a schedule and a spawn: an agent
that cannot be run by hand is not fired by its schedule or spawned by another agent.
Listing Aetherfy run history
GET /api/v1/agents/{agent}/runs → 200, newest first.
curl -s "https://agents.aetherfy.com/api/v1/agents/reporter/runs?limit=50&trigger_source=cron" \
-H "Authorization: Bearer $AETHERFY_API_KEY"| Parameter | Default | Notes |
|---|---|---|
limit | 20 | Clamped to 1–100 |
trigger_source | both | cron or manual |
before | — | The cursor of the last run you saw; returns the runs after it |
[
{
"id": "3c9a5f10-7b2e-4d81-a6f4-19e0c8b7d224",
"trigger_source": "cron",
"state": "completed",
"created_at": "2026-08-19T02:00:00Z",
"error_message": null,
"machine_started_at": "2026-08-19T02:00:07Z",
"machine_stopped_at": "2026-08-19T02:01:31Z",
"duration_seconds": 84,
"has_result": true,
"result_error": null,
"cursor": "WyIyMDI2LTA4LTE5VDAyOjAwOjAwKzAwOjAwIiw0Ml0"
}
]state is the deployment state, so a finished run reads completed and a failed one
failed with error_message set. The timing fields are the machine’s real
wall-clock, and duration_seconds is null until the run has both started and
stopped. A run of a type: service agent holds no machine of its own, so its two
machine fields are always null, and its duration_seconds runs from the moment the
service’s machine accepted the run to the moment its outcome was reported.
A run’s id is also its deployment id, which is what makes the rest of the run
inspectable: pass it as deployment_id to the logs route
to read just that run’s output, or to
the payload route to see the input it was started with.
has_result says whether the run returned a result;
this history never carries the values themselves, because it can hold a hundred
runs. Read the one you want from
GET /deployments/{deployment_id}. result_error
does ride along, since it is a short reason and finding the run that went wrong
is what this list is for.
Spawned runs are deliberately excluded. Only runs this agent triggered itself —
cron and manual — appear here. A run created by another agent spawning this one
belongs to the parent’s story and trigger_source=spawn is not addressable on this
route.
Page with before, not with an offset: pass the cursor of the last row you saw.
Treat it as opaque. Runs created in the same instant can fall on either side of a
page boundary, and the cursor is what places each of them on exactly one page.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_RUNS_INVALID_TRIGGER_SOURCE | 422 | Carries allowed: ["cron","manual"] |
AGENT_RUNS_INVALID_BEFORE | 422 | before was not the cursor of a run from this list |
Pausing and resuming an Aetherfy scheduled task
POST /api/v1/agents/{agent}/schedule/pause → 200
POST /api/v1/agents/{agent}/schedule/resume → 200
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/schedule/pause \
-H "Authorization: Bearer $AETHERFY_API_KEY"{"cron_paused": true, "cron_next_run_at": "2026-08-20T02:00:00Z", "changed": true}Both are idempotent: pausing an already-paused schedule answers 200 with
changed: false and alters nothing.
This is an operational pause, like stopping an agent — not a configuration change.
The schedule stays declared in aetherfy.yaml, so deploying again re-applies what
that file says, and deploying schedule: null clears the pause along with the
schedule.
Resuming never backfills. Aetherfy recomputes the next occurrence from the moment you resume, so occurrences that elapsed while paused are simply not run — a task paused for a week runs once when resumed, not seven times. Resuming a schedule that was already live leaves the existing cursor untouched, so a redundant resume cannot shift the next fire.
To change when a task runs, edit the schedule: expression and redeploy; these
routes only pause and resume it. See Scheduled tasks.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_SCHEDULE_NOT_SET | 422 | The agent has no schedule to pause or resume |
AGENT_NOT_FOUND | 404 | |
AGENT_OPERATION_IN_PROGRESS | 409 | A worker holds the agent. Transient — retry |
Spawning an Aetherfy worker agent
POST /api/v1/agents/{agent}/spawn → 202
{agent} is the parent. The body names the child to start. Called with the
AETHERFY_API_KEY Aetherfy injects into an agent, what this route allows is in
the table of agent-scoped keys.
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/orchestrator/spawn \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"child_agent_id":"reporter","payload":{"customer":"acme"}}'| Field | Type | Notes |
|---|---|---|
child_agent_id | string | Required. UUID or name of the agent to spawn, of either type |
payload | object | Input the child reads back at GET /deployments/{spawn_id}/payload |
{
"spawn_id": "b41e77c9-58a0-4de6-9a72-3c1f8e05d6b7",
"job_id": "d0f2a613-9c47-4b85-8e30-71a5c9f24e10",
"child_agent_id": "9a3f0c58-6b21-4e79-bd04-8f2e1c73a065",
"workspace": "research",
"region": "iad",
"status": "queued",
"estimated_start": "2026-08-19T09:15:02Z"
}workspace is the child’s own workspace, by name; never null, since every agent
lives in one. A spawn never moves the child into the parent’s workspace.
spawn_id is the child’s deployment id — the value Aetherfy injects into the child as
AETHERFY_SPAWN_ID, and the one to use when reading its payload or looking the run
up. A type: job child starts in the same region as its parent. A type: service
child’s run goes to a region where the service has a running machine: the parent’s
region when it has one there, otherwise another of its release’s regions, and
region in the response says which. A service with no running machine anywhere is
refused with 409 AGENT_SERVICE_NOT_RUNNING, whose message and detail.regions name
the regions its release is in.
Spawning requires configuration on both sides: the parent’s aetherfy.yaml needs
spawn.enabled: true and must list the child in spawn.workers (or list ["*"],
meaning any of your agents), and the child must already be deployed. An empty list allows no spawns;
why the list is explicit is on
the aetherfy.yaml page.
The child can be of either type: a type: service child’s run
is a request to its POST /aetherfy/run. Spawning records the parent on the run, not
on the child, so a child that has a schedule of its own keeps it. Chains are bounded by a maximum
spawn depth.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_NOT_SPAWN_ENABLED | 403 | Set spawn.enabled in the parent’s aetherfy.yaml, and deploy it |
AGENT_WORKER_NOT_ALLOWED | 403 | The child is not in the parent’s spawn.workers, and the list is not ["*"]. An empty list refuses every child |
AGENT_SPAWN_DEPTH_INVALID | 400 | The chain is too deep |
AGENT_PARENT_NOT_SPAWNABLE | 409 | The parent’s state does not allow spawning |
AGENT_PARENT_PAUSED | 409 | Start the parent |
AGENT_WORKER_PAUSED | 409 | Start the child |
AGENT_RUN_INELIGIBLE_STATE | 409 | The child’s status does not allow a run — usage_paused, stopped, archived and the like. Carries current_state |
AGENT_CHILD_NOT_DEPLOYED | 400 | Deploy the child first |
AGENT_PARENT_NO_DEPLOYMENT | 400 | The parent has never deployed |
AGENT_RUN_CONCURRENCY_LIMIT_EXCEEDED | 429 | Too many task runs in flight for this account |
AGENT_SERVICE_NOT_RUNNING | 409 | The service child has no running machine in any region. Start or redeploy it |
AGENT_SPAWN_RATE_LIMITED | 503 | A deploy of the same agent held its version lock too long. Transient — retry |
SOFT_CAP_EXCEEDED | 403 | A spend limit was reached |
The two throttles are different and want different handling. The 429 is your plan’s
cap on task runs in flight across the whole account, and it counts every run, not only
spawned ones: a run you started by hand and a run a schedule fired occupy the same
budget. detail.limit reads max_in_flight_runs, with detail.max_in_flight_runs (the
cap) and detail.in_flight_count (where you are) beside it, and the per-plan numbers
are on Limits. It also carries where to go next, the same field the
plan-limit 403 carries: detail.upgrade_url on a plan that can be upgraded,
detail.support_url on Performance, and neither on Enterprise, whose cap is set on the
contract. Nothing is queued and no run row is created — wait
for some to finish. The 503 means another
deploy of the same agent was holding its version lock when this spawn asked for one.
Aetherfy serialises that allocation, so this is not a collision and nothing was
corrupted — the spawn simply gave up waiting. Retry it; if the same spawn keeps being
refused, a deploy on that agent is stuck rather than slow.
Reading Aetherfy agent logs
GET /api/v1/agents/{agent}/logs → 200, an array of log entries.
curl -s "https://agents.aetherfy.com/api/v1/agents/reporter/logs?tail=200&level=ERROR" \
-H "Authorization: Bearer $AETHERFY_API_KEY"| Parameter | Default | Notes |
|---|---|---|
tail | 50 | Lines to return, up to 1000 |
since | — | Relative window, N followed by s, m, h or d: 45s, 30m, 1h, 7d |
level | — | One of INFO, WARN, ERROR, DEBUG, SYSTEM |
stream | — | stdout, stderr or system |
search | — | Case-insensitive substring match on the message |
deployment_id | — | Narrow to one deployment — the way to read a single run’s output |
after_id / before_id | — | Keyset cursors on the integer id |
The sort order depends on after_id. Without it you get newest-first
(descending id), which is what you want for “show me the last 200 lines”. Pass
after_id and the order flips to oldest-first (ascending), so that following a live
agent moves forward through time. A client that assumes one order will silently
reverse when it starts paging.
[
{
"id": 918273,
"timestamp": "2026-08-19T02:00:41Z",
"stream": "stdout",
"level": "INFO",
"message": "report generated for acme"
}
]level buckets several stored spellings: WARN covers WARNING, ERROR covers
CRITICAL and FATAL, and DEBUG covers TRACE. Those five bucket names are the
only accepted values.
These are the agent’s runtime logs. Build output is a different thing and is not available through the API — see Deployments.
To read one scheduled or spawned run’s output, take the run’s id from the run
history above and pass it as deployment_id.
Retention is 7 days on every plan. See Limits.
| Code | HTTP | Meaning |
|---|---|---|
AGENT_LOGS_INVALID_SINCE | 400 | since was not a recognised duration |
AGENT_LOGS_INVALID_FILTER | 422 | level or stream was not one of the accepted values |
AGENT_LOGS_INVALID_DEPLOYMENT_ID | 422 | deployment_id was not a UUID |