> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.aegi.run/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.aegi.run/_mcp/server.

# Asynchronous jobs

Four features take longer than a single HTTP request should: [image filters and overlays](/images), [website screenshots](/tools), and [Minecraft renders](/minecraft). Rather than hold the connection open, Aegis queues the work
and answers immediately with a `jobId` you collect the result from.

```ts
const shot = await aegis.tools.screenshot({ url: 'https://aegi.run' })
// { jobId: '3f8a...', status: 'pending', url: 'https://aegi.run' }

const { result } = await aegis.jobs.wait(shot.jobId, { timeout: 60 })
```

## Job types

| Feature                             | Job `type`            |
| ----------------------------------- | --------------------- |
| [Image filter](/images) and overlay | `filters`, `overlays` |
| [Website screenshot](/tools)        | `screenshot`          |
| [Minecraft render](/minecraft)      | `mcrender`            |

## The job object

Both endpoints answer with the same record:

```json
{
  "data": {
    "id": "3f8a1c8e-5b7d-2049-af31-c6e8d5b90742",
    "type": "screenshot",
    "status": "completed",
    "result": { "url": "https://files.aegi.run/9c1e..." },
    "createdAt": 1770643920000,
    "updatedAt": 1770643934000
  }
}
```

`status` moves through:

| Status       | Meaning                   |
| ------------ | ------------------------- |
| `pending`    | Queued, not started yet   |
| `processing` | A worker has picked it up |
| `completed`  | Finished; read `result`   |
| `failed`     | Stopped; read `error`     |

`result` is typed `unknown` in the spec on purpose — its shape is decided by the
render worker rather than the API, so it differs per job type and is not
pinned to a schema. Read it defensively. `payload` is the original job
parameters, echoed back for traceability.

> **Note**
>
> Music never uses jobs. A player is long-lived state you drive directly, not
> work that finishes — see [Music](/music).

## Waiting instead of polling

`GET /v1/jobs/{id}/wait` holds the request open until the job reaches a final
state, so you spend one request instead of a polling loop.

```ts
const job = await aegis.jobs.wait(jobId, { timeout: 60 })
```

`timeout` is in seconds, between 1 and 300, and defaults to 30.

> **Warning**
>
> A wait that times out returns `504` with the **job still attached** — the same
> body as a success, just a different status. Check the status code, or catch
> the `AegisError` the SDK raises for it, and the job may well have finished
> moments later.

## Polling directly

`GET /v1/jobs/{id}` is a plain read and never blocks. Poll it at a sensible
interval rather than in a tight loop:

```ts
let job = await aegis.jobs.get(jobId)

while (job.status === 'pending' || job.status === 'processing') {
  await new Promise((r) => setTimeout(r, 1000))
  job = await aegis.jobs.get(jobId)
}

if (job.status === 'failed') throw new Error(job.error)
```

Polling costs you a `cheap` class request each time, which is metered — see
[rate limits](/rate-limits). Prefer `wait` when you can.

## Expiry

Job records are kept for 24 hours. After that, both endpoints answer `404` with
`"Job not found"`, and a `jobId` is no longer collectable.

> **Note**
>
> The rendered image itself is a separate artefact and outlives the job record.
> Copy the `result` URL when the job completes if you need it later.