---
title: "Run an agent loop"
method: POST
path: "/computers/{id}/agent"
tags: ["Agents"]
---

# Run an agent loop

`POST /computers/{id}/agent`

## Request

- Base URL: `https://app.mandala.computer/api/v1`
- URL: `https://app.mandala.computer/api/v1/computers/{id}/agent`
- Auth: HTTP bearer

One call that drives this computer until the task is done: the loop screenshots, asks a model what to do, performs it, and repeats. Answered by the control plane rather than forwarded to a hypervisor, and it can run for minutes.

The computer must already be RUNNING. This endpoint will not start one for you: starting is billable, and it is not a decision to make on somebody’s behalf because they sent a prompt. A stopped or suspended computer is refused with status 409, and so is one another agent run is already driving — that one with `reason: "contention"`, because it ends when the other run does, as does one whose disk is still being made.

WITH STREAMING, THE DEFAULT, THOSE REFUSALS ARRIVE ON A 200. Every refusal found after the request is accepted — an unknown computer (404), one not running or already driven (409), a credential refused part way through, the model API’s own errors, and on an OpenAI run a batch, `bash` command or screenshot that failed — arrives on the stream as `event: error` with `{request_id, error, status, reason?, cost_usd?, top_up_url?, usage, steps}`, where `status` is the status it would have had and `steps` is the LIST of `AgentStep`s taken before it failed — the list `stream: false` calls `steps_taken`, not the count `AgentResult.steps` carries on `done` (an unexpected failure of the stream itself sends `{request_id, error, status: 500}` alone). Only `stream: false` answers them as HTTP statuses. What is checked before the request is accepted — the key, the role, the rate budget, the body, `X-Model-Key` — is an HTTP status either way.

Send `X-Model-Key` to use your own Anthropic or OpenAI key. The key is never stored; the provider bills you directly and no Mandala AI credits are spent. Select OpenAI with a gpt-*, o-series or computer-use-preview model, or provider: openai. A key whose prefix belongs to the other provider — an sk-ant- key for OpenAI, or any other sk- key for Anthropic — is a 400. OpenAI uses the Responses API with native computer batches and a bash function on this desktop. It sends prompts, screenshots and command output to OpenAI, using stored responses to continue the run. Each OpenAI batch is admitted in full against max_steps and the rate budget before any action runs; a batch that exceeds the remaining allowance is not partially executed. A batch, `bash` command or screenshot that fails ends an OpenAI run as a failure rather than with a `stop`: it carries the status, message and any `reason` the platform answered that call with, and the steps already taken — for a batch, the actions the desktop acknowledged, and the failed one with its `error` when the desktop said which it was. Nothing is replayed, so inspect the desktop before continuing. A command that runs and exits non-zero is not a failure; its output goes back to the model. OpenAI requires your own key. Funded runs are metered by Mandala. Without that header, eligible accounts can use funded AI credits when enabled. Funded runs require a plan, a supported Stripe billing country, and at least $0.25 available for the first model call; otherwise they are refused before streaming (402 with `top_up_url` for insufficient credits; 400, 403 or 503 otherwise, as each of those statuses says). Funded runs use the platform system prompt and tools, default to `claude-sonnet-5-5`, and also allow `claude-opus-5-5`. Custom system prompts and other initially selected models require your own key. Anthropic default fallback routing is enabled and can serve a broader model; each billed attempt uses that model’s rate. Each model call reserves $0.25, then settles actual usage at 1.3× list price (deployment configurable). A completed call can overrun its reserve or max_cost_usd; its full cost is charged before another call is admitted. Usage beyond remaining credits creates debt that survives credit expiry; future grants and top-ups settle it first. If credits become unavailable mid-run it ends with `credits_exhausted`; the cost cap ends with `max_cost`. Solo/Studio/Swarm monthly grants are $5/$10/$50 and expire at billing-period end. $10/$25/$50 top-ups expire 12 months from purchase. Without a plan, existing credits freeze while their original expiry continues; a purchase paid after plan removal is retained for issuance on resubscription if unexpired. Funded prompts, screenshots and tool results are sent to Anthropic under Mandala’s account. See [pricing](https://mandala.computer/pricing), [terms](https://mandala.computer/terms) and [subprocessors](https://mandala.computer/subprocessors).

The response STREAMS as `text/event-stream` by default, because a run can go a long time between anything worth saying and a buffered answer would be a minute of silence followed by everything at once. Send `stream: false` for a single JSON body at the end instead. On the hosted API a request that is not streaming is cut off at the edge after about 120 seconds (a `524` with no body), and the run’s result — its usage and the steps it took — is lost with it. The run is stopped where it was once the dropped connection reaches the platform, so do not assume it finished. Keep streaming, the default, for anything that may take longer.

A RUN IS KEPT IN A THREAD. Unless you send `store: false`, a run that acted on the desktop, or finished with an answer, is kept as one exchange — the prompt, and the run’s closing text and how it ended (`aborted` for one stopped part way, which may have done more than it reported) — in a thread on this computer, and its `thread_id` comes back on `done` (or the `stream: false` body) and on a failure’s `error`. Send that `thread_id` with the next prompt to continue the conversation: the model is given the earlier turns (the newest 64,000 characters of them, with a note when some are left out) and told they are done, and acts only on the new prompt. A run refused before it acted keeps nothing and starts no new thread; a continuation still names the thread you sent, while it exists. An account keeps 1000 threads: a new one takes the place of the least recently used that this key could delete, and when there is none (a workspace key whose workspace holds none, say) the run is not kept and has no `thread_id` — delete threads you no longer need. Threads are listed and read under `/agent/threads`, and are deleted with their computer.

A CLAUDE RUN CAN ZOOM. Except on claude-opus-4-5 to 4-7 and claude-sonnet-4-6, which use the older computer tool, the loop serves the computer toolset’s `zoom`: a close-up cut from a fresh capture of the screen, of the rectangle the model names in the last full screenshot’s coordinates, so it shows the screen as it is now, which can have changed since that screenshot. A zoom is a step (`action: "zoom"`) and counts toward `max_steps`. One that is refused — no full screenshot shown yet, a rectangle outside it, or a capture replaced before it could be cropped — is a step with `error`, and the loop continues. OpenAI runs have no zoom.

EVERY ACTION THE LOOP TAKES SPENDS YOUR RATE BUDGET, the same budget your own calls draw on. An Anthropic step costs what the calls it makes would cost you: an input and the screenshot that shows it, one call for `bash`, a cursor read or a bare screenshot, and two screenshots for a `zoom`. A `key` action with `repeat: n` is one input per press and then the screenshot, n + 1 calls, so a single step can cost up to 101 (`repeat` is at most 100). OpenAI reserves, before each batch runs, the price of one input call for every action in it plus one screenshot for the screenshot after it; its initial screenshot and each `bash` call cost one call each. A run that exhausts it stops where it is and ends with `stop: "rate_limited"` rather than failing: the steps already taken are reported, and the work already done to the desktop stands. Treat it the way you treat `max_steps` — as a run that did not finish — and wait rather than raising anything.

A RUN CAN BE REFUSED PART WAY THROUGH. The API key it is running on is re-checked before every model call and before every action or OpenAI batch, so a key deleted — or a membership removed — mid-run stops the run where it is. That arrives as an `error` event on the stream (or the single JSON body when `stream` is false) carrying `reason: "revoked"` beside `error`, plus the usage and the steps already taken, which are real and are billed. Most of what this endpoint refuses carries NO word: the model API’s own failures and a stopped or suspended computer arrive without one. A computer another run is already driving, like one whose disk is still being made, carries `reason: "contention"`. The run’s opening computer lookup, and an OpenAI run’s batch, `bash` command or screenshot, forward whatever the platform classified its own refusal as, so any of the words above can reach you through this frame. Read it the way you read `reason` anywhere else here, and treat an absent word as no classification.

Requires the `member` role or higher.

## Path parameters

- `id` string, required

## Headers

- `X-Model-Key` string

## Request body

- object
  - `prompt` string, required: What you want done, in plain language. Required.
  - `system` string: A system prompt for the model. Requires your own X-Model-Key.
  - `max_cost_usd` number: Funded run admission budget in USD. Actual cost can exceed this by the final model call; BYOK ignores it.
  - `max_steps` integer: How many actions the loop may take before it stops. Defaults to 20, and 100 is the most it accepts — a value past that is a 400 rather than a quietly shorter run. A STEP IS ONE ACTION ON THE DESKTOP, not one exchange with the model, and the two do not line up in either direction: one model reply may ask for several actions and spends a step on each, while a reply that asks for none — or a paused turn, resubmitted — costs model tokens and no step at all. Nor does every step take a screenshot; a `bash` call or a cursor read does not, and a `zoom` is a step that takes two. So this bounds the WORK, and bounds your Anthropic spend only loosely. For funded runs, max_cost_usd limits admission but the final call can overrun it. For BYOK, manage spend with your provider. A run that reaches max_steps ends with `stop: "max_steps"` and the work already done to the desktop stands. `null` is the same as leaving it out.
  - `provider` 'anthropic' | 'openai': Optional provider. Inferred from claude-*, gpt-*, o-series and computer-use-preview model ids; defaults to Anthropic when neither is supplied. Required for an unknown model namespace. A recognized model/provider mismatch is refused. OpenAI requires X-Model-Key.
  - `model` string: A model id for the selected provider. Funded default: claude-sonnet-5-5; claude-opus-5-5 is also selectable. BYOK supports Anthropic and OpenAI computer-capable models. Defaults: claude-opus-5-5 for Anthropic, gpt-6.1-sol for OpenAI. Funded Anthropic fallback routing can use other models.
  - `stream` boolean: Send `false` for one JSON body at the end instead of an event stream. **This endpoint streams by default**, so omitting it gives you `text/event-stream`. Only the JSON booleans `true` and `false` change the default; any other value, the string `"false"` included, is read as if it were left out. On the hosted API a request that is not streaming is cut off at the edge after about 120 seconds (a `524` with no body), and the run’s result — its usage and the steps it took — is lost with it. The run is stopped where it was once the dropped connection reaches the platform, so do not assume it finished. Keep streaming, the default, for anything that may take longer.
  - `thread_id` string: A thread to continue: `thr_` and twenty-four hex characters, from an earlier run’s `thread_id`. Its earlier turns are given to the model as the conversation so far, and this run’s exchange is added to it. A thread of another computer is a `409`, as is a full one (100 turns); one this key cannot see is a `404`.
  - `store` boolean: Whether to keep this run’s exchange in a thread. Default `true`: a run that does something is kept, in the thread you named or a new one, and its `thread_id` comes back on the result. `false` keeps nothing, and cannot be sent with `thread_id`.
  - `messages` object[]: The conversation so far, oldest first and without this run’s `prompt`, for a caller that keeps its own rather than a thread here. Given to the model as earlier turns, which it is told are done. Cannot be sent with `thread_id`. A new thread starts with these turns.
    - `role` 'user' | 'assistant', required
    - `content` string, required

## Response `200`

An SSE event log of what the loop did, or one JSON body when `stream` is false.

The stream’s events are `step` (an `AgentStep`, one per action), `text` (`{text}`, what the model said), `done` (the fields below without `steps_taken`: the run finished or stopped) and `error` (see above: the run failed; its `steps` is an `AgentStep` list, not a count). With `stream: false` the answer is the fields below, `steps_taken` included, and a run that fails answers its status with an `AgentFailure` body — `{error, reason?, cost_usd?, top_up_url?, usage, steps_taken}`.

The stream opens at once with a `: run starting` comment line, before the first model call, and sends `: keepalive` comment lines while the run goes quiet. Lines beginning `:` carry nothing and can be ignored.

- AgentResult
  - `cost_usd` number: Cumulative AI credit charge in USD, present on funded runs only.
  - `steps` integer, required: How many actions the loop took — a count; the actions themselves are `steps_taken`.
  - `stop` string, required: Why the loop ended: `end_turn` (the model finished), `max_steps` (it reached `max_steps`), `refusal` (the model declined), `pause_turn` (the model kept pausing its turn), `rate_limited` (your rate budget ran out mid-run), `credits_exhausted` (funded credits unavailable), `max_cost` (the next reservation would exceed max_cost_usd), or another stop reason the model itself gave, such as `max_tokens`. OpenAI also reports `incomplete` for an unfinished response and `safety_check` when action approval is needed. Only `end_turn` means the task was finished; the others are a run that stopped, and the work it did to the desktop stands.
  - `text` string, required: The model’s closing text — its answer, or why it could not get there.
  - `usage` AgentUsage, required
    - `input_tokens` integer, required: Input tokens, the cache reads and writes below included.
    - `output_tokens` integer, required
    - `cache_read_tokens` integer, required
    - `cache_write_tokens` integer, required: Provider-reported cache writes; zero only when absent. OpenAI reads input_tokens_details.cache_write_tokens.
  - `thread_id` string: The thread this exchange was kept in; send it as `thread_id` to continue. Absent with `store: false`.
  - `steps_taken` AgentStep[]: Every action, in order. On the `stream: false` body only; the stream’s `done` event carries the other fields, and each action arrived before it as a `step` event.
    - `cost_usd` number: Cumulative funded run charge in USD. Multiple tools in one reply repeat the same total; do not sum these fields.
    - `credits_remaining_usd` number: Available account AI credits after this model call; may be negative after an overrun. Omitted if the post-debit balance lookup is unavailable.
    - `n` integer, required: Its position in the run, from 1.
    - `tool` string, required: The tool the model reached for, e.g. `computer` or `bash`.
    - `action` string: The provider-native action within it, e.g. `left_click`, `zoom` or OpenAI `click`. Absent for `bash`.
    - `detail` string: What the platform did with it, in one line.
    - `error` string: Present when an action failed or was refused. On an Anthropic run the model is told and the loop goes on. On an OpenAI run a failed batch or `bash` command ends the run as a failure, after the actions the desktop acknowledged; input is not replayed.

## Other responses

- `400`: The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly. Also a funded run (no `X-Model-Key`) that this account cannot have: funded AI is off or not available to the account, the body sends `system`, names a model other than claude-sonnet-5-5 or claude-opus-5-5 (an OpenAI model included), or sets `max_cost_usd` outside 0.25 to 20. Send your own key as `X-Model-Key` instead. Answered before streaming. On an OpenAI run, also a `400` the platform answered a batch, `bash` command or screenshot the model asked for, relayed as the run’s failure.
- `401`: Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). On this route a `401` can also be the model provider’s. With `X-Model-Key`, your key was refused; on a funded run (no `X-Model-Key`) it is the platform’s own provider credential, the message is `Mandala AI is unavailable`, and there is nothing on your side to fix. Only its status is kept: the body is this route’s own error shape, with a sentence of ours rather than the provider’s, and none of the provider’s headers come with it.
- `402`: Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed. Insufficient funded AI credits before streaming: an `AgentCreditRefusal`, `{error: "Insufficient AI credits", top_up_url: "/billing"}`, where `top_up_url` is a path on this origin rather than a full URL. If credits can no longer be held when the run makes its first model call, the same refusal and `top_up_url` arrive as the run’s failure instead, an `AgentFailure`. For BYOK, the model API’s own billing refusal is relayed on the run as an AgentFailure. On an OpenAI run, so is a `402` the platform answered a batch, `bash` command or screenshot with: your plan refusing to resume a computer that was suspended during the run, whose message names the limit.
- `403`: The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: "revoked"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help. Also a funded run for an account with no billing customer or an unsupported billing country (`A supported billing country is required; use your own key.`). Answered before streaming.
- `404`: No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.
- `405`: This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.
- `409`: Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running. WHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them. Where the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline. Every 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names. The ones worth knowing by name, because each has a next step that is not "wait": A resize past what the computer’s host can run carries `move`, an object rather than a message: `{"required":true,"possible":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change. The move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: "contention"` — one move runs per account, and `GET /moves` says which. And the terminal socket refuses twice over: `reason: "unavailable"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: "unsupported"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.
- `413`: What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.
- `429`: You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: "contention"` (see `POST /snapshots/{id}/clone`). On this route a 429 is either your rate budget here (an `Error` body) or the model provider’s rate limit on your `X-Model-Key` — Anthropic’s `rate_limit_error` or OpenAI’s 429 — relayed with its status (an `AgentFailure` body). A funded run relays a 429 from Anthropic on Mandala’s account the same way, with the message `Mandala AI is unavailable`; a spend limit on that account is a 503 instead. The relayed one carries the model API’s own `Retry-After`, in whole seconds, when that API sent one, and may carry none: no wait is invented for it. Streaming, the run’s 429 arrives as a stream event and carries no header.
- `500`: The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.
- `502`: The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification. The model API could not be reached, or the call to it was cut off before it answered. An OpenAI call also ends the run with 502 when OpenAI answers with a 5xx, sends a response that cannot be read or that this platform cannot act on, or takes longer than 120 seconds. On an OpenAI run it can also come from the desktop: a `502` the platform answered a batch, `bash` command or screenshot with — the guest agent not answering, for one — is relayed, and the run answers `502` itself when the desktop returns a screenshot it cannot read or incomplete batch progress, or a call to it fails without an answer. Input is not retried: read `steps_taken` and look at the desktop before continuing.
- `503`: Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead. Also funded AI being unavailable. Before streaming: `Mandala AI is unavailable`, or `Billing country could not be verified; use your own key or try again.` During a funded run: Mandala’s spend limit with the model provider was reached, or a model call could not be priced or recorded. The run ends with `Mandala AI is unavailable`, and the calls already made stay charged. Also `reason: "draining"`, with a `Retry-After` header: the platform is about to restart for an update and is not starting new agent runs for a few minutes. Runs already going are not affected. Nothing was started, held, charged or recorded for this request, so send the same one again after `Retry-After` seconds. Answered before anything streams, as an `Error` body.
- `504`: The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again. On this route, Anthropic’s own `timeout_error` from the model API, relayed with its status: the model call took too long, and the run stopped there. An OpenAI call that times out is a 502 instead.
- `529`: The model API is overloaded (`overloaded_error`), relayed with its status intact. Not a status this platform sends of its own; the run stopped there, and a retry later can succeed. With your own Anthropic key, every status the model API answers reaches you as itself, with a fixed message naming the provider and status, in the `AgentFailure` body. OpenAI’s 4xx statuses reach you as themselves with a fixed message naming the status, and its 5xx as 502. A funded run relays Anthropic’s status with the message `Mandala AI is unavailable`, except that a spend limit on Mandala’s account is a 503.

---

[API](https://skmtc.dev/mandala/apis/mandala-computer-api.md) · [All operations](https://skmtc.dev/mandala/apis/mandala-computer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mandala/apis/mandala-computer-api/revisions/2afbb87fa703?raw)
