---
slug: agents/api-runs
title: Runs, schedules and spawns API
kind: reference
surface: agents
summary: REST reference for running Aetherfy agents on demand — triggering a run, listing run history with keyset pagination, pausing and resuming a scheduled task, spawning a worker agent with a payload, and querying runtime logs.
sources:
  - aetherfy-control-plane:api/routes/agents.py
  - aetherfy-control-plane:shared/error_codes.py
  - aetherfy-control-plane:workers/cron_scheduler_worker.py
---

# 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](/agents/api). `{agent}`
accepts an agent UUID or its name throughout.

## Running an Aetherfy agent on demand

`POST /api/v1/agents/{agent}/run` → 202

```bash
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](/agents/api-deployments). Omit it entirely to run with no input.

```json
{
  "deployment_id": "3c9a5f10-7b2e-4d81-a6f4-19e0c8b7d224",
  "version": 12,
  "job_id": "8e41b0c7-2d15-4a93-b7e2-5f0c6a91d338"
}
```

Only `type: job` agents can be run this way — a `type: service` agent is always on and
has nothing to trigger. The agent must already be deployed; run does not build.

A manual run and a [scheduled task](/agents/scheduled-tasks) 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_RUN_REQUIRES_JOB_TYPE` | 422 | Set `type: job` in `aetherfy.yaml` and redeploy |
| `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_IN_PROGRESS` | 409 | A run is already in flight. **Transient — retry** |
| `SOFT_CAP_EXCEEDED` | 403 | A spend limit was reached; new runs are frozen |

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

## Listing Aetherfy run history

`GET /api/v1/agents/{agent}/runs` → 200, newest first.

```bash
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` | — | ISO-8601 cursor; returns runs created strictly before it |

```json
[
  {
    "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
  }
]
```

`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'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](#reading-aetherfy-agent-logs)
to read just that run's output, or to
[the payload route](/agents/api-deployments) to see the input it was started with.

**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 `created_at` of the last row you saw.

| Code | HTTP | Meaning |
|---|---|---|
| `AGENT_RUNS_INVALID_TRIGGER_SOURCE` | 422 | Carries `allowed: ["cron","manual"]` |
| `AGENT_RUNS_INVALID_BEFORE` | 422 | `before` was not an ISO-8601 timestamp |

## Pausing and resuming an Aetherfy scheduled task

`POST /api/v1/agents/{agent}/schedule/pause` → 200
`POST /api/v1/agents/{agent}/schedule/resume` → 200

```bash
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/schedule/pause \
  -H "Authorization: Bearer $AETHERFY_API_KEY"
```

```json
{"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](/agents/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.

```bash
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 `type: job` agent to spawn |
| `payload` | object | Input the child reads back at `GET /deployments/{spawn_id}/payload` |

```json
{
  "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"
}
```

`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. The child starts in the **same region as its parent**.

Spawning requires configuration on both sides: the parent needs `spawn_enabled`, the
child must be `type: job` and already deployed, and if the parent lists
`allowed_workers` the child must be on that list. Chains are bounded by a maximum
spawn depth.

| Code | HTTP | Meaning |
|---|---|---|
| `AGENT_NOT_SPAWN_ENABLED` | 403 | Set `spawn_enabled` on the parent |
| `AGENT_WORKER_NOT_ALLOWED` | 403 | The child is not in the parent's `allowed_workers` |
| `AGENT_CHILD_NOT_JOB_TYPE` | 400 | Only `type: job` agents can be spawned |
| `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_CHILD_NOT_DEPLOYED` | 400 | Deploy the child first |
| `AGENT_PARENT_NO_DEPLOYMENT` | 400 | The parent has never deployed |
| `AGENT_SPAWN_CONCURRENCY_LIMIT_EXCEEDED` | 429 | Too many spawns in flight for this account |
| `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 a hard cap on
concurrent spawns across your account — 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.

```bash
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.

```json
[
  {
    "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](/agents/api-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](/platform/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 |
