---
title: "Runs"
description: "Read an agent run: its status, its approvals, its events, its cost, and how to stop it."
url: https://docs.roasos.com/agent/runs
---

# Runs

One family reads every agent run: `GET /v1/agent-runs/{id}`. An ask, a skill run, a workflow run, and a scheduled run use it.

| Method and path                                    | Returns                                                   |
| -------------------------------------------------- | --------------------------------------------------------- |
| `GET /v1/agent-runs/{id}`                          | The full run                                              |
| `GET /v1/agent-runs/{id}/status`                   | `{ id, status, updated_at }` for the poll loop            |
| `GET /v1/agent-runs/{id}/result`                   | The answer of the agent, when the run is terminal         |
| `GET /v1/agent-runs/{id}/events`                   | The ordered lifecycle of the run                          |
| `POST /v1/agent-runs/{id}/cancel`                  | Stops a run that is not terminal                          |
| `POST /v1/agent-runs/{id}/gates/{gate_id}/approve` | Approves one open approval, and starts the work behind it |
| `POST /v1/agent-runs/{id}/gates/{gate_id}/decline` | Refuses one open approval                                 |

A run in a different workspace returns `404 agent_run_not_found`. It never returns `403`.

## The statuses

| Status       | Meaning                                                 |
| ------------ | ------------------------------------------------------- |
| `quoted`     | A quote only. Nothing is reserved, and nothing runs.    |
| `processing` | The run works.                                          |
| `canceling`  | You asked the run to stop. The run has not stopped yet. |
| `completed`  | The run finished. The result is ready.                  |
| `failed`     | The run stopped with an error.                          |
| `canceled`   | You stopped the run.                                    |

The three last values are terminal. A run keeps the same id in each status.

A run in `canceling` carries a `cancel_requested_at` timestamp. The run stops at
the end of the step that it works on. It then reads `canceled`. A run that
finishes that step first reads `completed`.

## The run envelope

```json
{
  "data": {
    "schema_version": "agent-run@1",
    "kind": "agent_run",
    "id": "3d9c17aa-...",
    "trigger": "skill",
    "skill_id": "7c2a3f10-...",
    "conversation_id": "1f0c8b52-...",
    "status": "processing",
    "max_credits": 25,
    "generation_spend_cap_credits": 40,
    "credits_reserved": 65,
    "pricing_version": "agent-token-pricing@2026-10-04.v3",
    "started_at": "2026-08-21T10:08:02.000Z",
    "pending_approvals": [
      {
        "gate_id": "44b1d0e7-...",
        "summary": "storyboard render x 3",
        "credits": 60,
        "expires_at": "2026-08-21T10:23:02.000Z",
        "approve_url": "https://www.roasos.com/home/1f0c8b52-..."
      }
    ],
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:09:20.000Z"
  }
}
```

`trigger` states what started the run: `api`, `skill`, `routine`, `workflow`, or `chat`. A typed door such as `POST /v1/workflows/remix` writes `workflow` and names its pack skill in `skill_id`.

A completed run carries `result_ready: true`. Read the answer at `GET /v1/agent-runs/{id}/result`. A quoted run carries `credits_max`, `quote_id` and `expires_at` instead, and it reserves nothing.

## Pending approvals

A run opens an approval for each paid action that it cannot approve itself. Refer to [Spend caps and approvals](/agent/ask-the-agent#spend-caps-and-approvals).

| Field         | Meaning                                                                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gate_id`     | The identifier of the approval. Use it on the approve path and on the decline path.                                                                   |
| `summary`     | A short description of the work.                                                                                                                      |
| `credits`     | The price of the work. The value is `null` when the price is not yet known.                                                                           |
| `items`       | Only on a batch card. One entry for each item on the card, with its `index`, its `summary` and its `credits`. Use `index` in `drop` when you approve. |
| `expires_at`  | The time when the approval stops. The limit is 15 minutes.                                                                                            |
| `approve_url` | Where to say yes. A run in a thread names its card in roasOS. A run with no thread names the approve path of this approval.                           |

An approval that expires does not run and does not charge you.

The list holds at most 20 open approvals. One approval card carries at most eight quotes. A card above that number is one that the run cannot approve by itself, so it waits for a person.

A run that starts from a skill or from a workflow writes no thread, so it has no card in roasOS. Approve that work over REST. Refer to [Approve an approval](#approve-an-approval).

## The events

```bash
curl https://api.roasos.com/v1/agent-runs/3d9c17aa-.../events \
  -H "Authorization: Bearer roasos_sk_..."
```

```json
{
  "data": [
    { "seq": 0, "event": "accepted", "at": "2026-08-21T10:07:44.000Z" },
    { "seq": 1000, "event": "started", "at": "2026-08-21T10:08:02.000Z" },
    { "seq": 2000, "event": "tool_called", "at": "2026-08-21T10:08:05.000Z" },
    { "seq": 2001, "event": "tool_called", "at": "2026-08-21T10:08:44.000Z" },
    { "seq": 3000, "event": "gate_opened", "at": "2026-08-21T10:09:01.000Z" },
    { "seq": 4000, "event": "gate_auto_approved", "at": "2026-08-21T10:09:01.000Z" },
    { "seq": 5000, "event": "generation_started", "at": "2026-08-21T10:09:02.000Z" },
    { "seq": 6000, "event": "settled", "at": "2026-08-21T10:11:09.000Z" }
  ]
}
```

The list holds nine names: `accepted`, `started`, `tool_called`, `gate_opened`, `gate_auto_approved`, `generation_started`, `settled`, `failed`, and `cancelled`.

`tool_called`, `gate_opened`, and `generation_started` occur many times in one run. Each name keeps its own band of numbers, and each occurrence takes the next number in that band. `seq` is therefore an address inside a band, and not a clock.

roasOS orders the list by `at` first, and by `seq` inside one timestamp. Read `at` for the time of a step.

The list takes no cursor and no limit. It gives the first 500 events of the run.

A run that is older than the timeline shows the steps that its own times can prove.

## What a run costs

roasOS reserves the ceiling of the run at the confirm step. It settles the run when the run stops. It refunds the part that the run does not use. A terminal run therefore carries a `usage` object.

```json
{
  "model": "anthropic/claude-sonnet-5",
  "pricing_version": "agent-token-pricing@2026-10-04.v3",
  "input_tokens": 18422,
  "cached_input_tokens": 12800,
  "output_tokens": 2140,
  "steps": 6,
  "tool_calls": 4,
  "credits_reserved": 65,
  "credits_charged": 2,
  "credits_refunded": 63,
  "generation_credits_charged": 40
}
```

| Field                        | Meaning                                              |
| ---------------------------- | ---------------------------------------------------- |
| `model`                      | The model that did the thinking of the run.          |
| `pricing_version`            | The rate table that made the charge.                 |
| `input_tokens`               | The tokens that the run sent to the model.           |
| `cached_input_tokens`        | The part of `input_tokens` that came from the cache. |
| `output_tokens`              | The tokens that the model wrote.                     |
| `steps`                      | The number of steps in the run.                      |
| `tool_calls`                 | The number of tools that the run called.             |
| `credits_reserved`           | The ceiling that the confirm step reserved.          |
| `credits_charged`            | The credits for the tokens of the run.               |
| `credits_refunded`           | The part of the reservation that roasOS gave back.   |
| `generation_credits_charged` | The credits that the paid work inside the run used.  |

Each generation inside a run quotes and reserves on its own. The run cap sits above those generations. It never replaces them.

<Callout>
  roasOS never estimates tokens to charge you. If the token record of a run is not available, roasOS charges 0 credits and refunds the reservation in full. The `usage` object then shows zero tokens.
</Callout>

## A failed run

A failed run gives the reason in `error`, and it refunds the reservation. Use `error.code` in your conditions. Do not parse `error.message`.

```json
{
  "data": {
    "schema_version": "agent-run@1",
    "kind": "agent_run",
    "id": "3d9c17aa-...",
    "status": "failed",
    "credits_reserved": 65,
    "completed_at": "2026-08-21T10:09:55.000Z",
    "error": { "code": "run_failed", "message": "The run stopped before it answered" },
    "usage": { "…": "…" }
  }
}
```

A run row carries one of three codes.

| Code                | Cause                                                |
| ------------------- | ---------------------------------------------------- |
| `run_failed`        | The turn stopped before it answered                  |
| `run_start_refused` | roasOS could not start the turn. Nothing ran.        |
| `run_timed_out`     | The run passed its time limit, and roasOS stopped it |

Each of the three refunds the reservation in full. A refusal that occurs before the `202` answers on the call itself. It makes no run, and it therefore has no code here. Refer to [Errors at the door](/agent/ask-the-agent#errors-at-the-door).

A run that stops in the middle keeps the generations that it already started. Those jobs have their own lifecycle. Read them at `GET /v1/jobs/{id}`.

## Approve an approval

`POST /v1/agent-runs/{id}/gates/{gate_id}/approve` releases one open approval and starts the work behind it. `approve_url` on the approval already holds this path when the run has no thread.

```bash
curl -X POST https://api.roasos.com/v1/agent-runs/6f31c07d-.../gates/44b1d0e7-.../approve \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: remix-glow-serum-001-approve" \
  -H "Content-Type: application/json" \
  -d '{ "generation_spend_cap_credits": 120, "note": "Ship both variants." }'
```

```json
{
  "data": {
    "gate_id": "44b1d0e7-...",
    "run_id": "6f31c07d-...",
    "status": "approved",
    "decided_at": "2026-08-21T11:12:40.000Z",
    "follow_up_run_id": "a71e5b28-..."
  }
}
```

| Field              | Meaning                                                                            |
| ------------------ | ---------------------------------------------------------------------------------- |
| `status`           | `approved`, `already_decided`, or `expired`                                        |
| `decided_at`       | The time of the decision. It is `null` when the approval expired.                  |
| `follow_up_run_id` | The run that the approval started. It is `null` when the approval started no work. |

The body is optional, and it carries four fields. `note` records why, in 400 characters or fewer. `max_credits` and `generation_spend_cap_credits` bound the follow-up run, exactly as they bound the first run. `drop` removes items from a batch card. Refer to [Remove items from a batch](#remove-items-from-a-batch).

roasOS composes the text of the follow-up run from the approved card. The caller never writes it. A run with no thread has no history, so the card is the only true record of what was approved.

The follow-up run carries `trigger: "api"`, and it keeps the `skill_id` and the `conversation_id` of the run that opened the card.

### Remove items from a batch

A batch card holds two to eight items of the same tool. Each item has its own price. The `items` list on the approval shows them. You can remove some items before you approve the card. This is the same as when you clear an item on the card in roasOS.

Put the `index` of each item that must not run in `drop`:

```bash
curl -X POST https://api.roasos.com/v1/agent-runs/6f31c07d-.../gates/44b1d0e7-.../approve \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: statics-glow-serum-001-approve" \
  -H "Content-Type: application/json" \
  -d '{ "drop": [1, 3] }'
```

* The items in `drop` do not run, and roasOS does not charge for them.
* roasOS checks the per-request ceiling of the key against the items that you keep.
* `drop` must keep one item or more. To refuse all of the card, decline it.
* An index that the card does not have answers `400 gate_drop_invalid`.
* A retry of an approval keeps the `drop` list of the first approval. A new list on the retry has no effect.

Only a card with a render behind it is approvable. A card that asks a question answers `409 agent_gate_not_approvable`. A card that asks for more headroom is one such card. Its answer is a higher cap on a new run, and never an approval. Decline it, or start the run again with a higher `generation_spend_cap_credits`.

The approval spends on your key. It quotes and confirms the follow-up run in the one call, so the body states no `quote_only` value and no `quote_id`.

| Rule                                                                    | Answer above the limit              |
| ----------------------------------------------------------------------- | ----------------------------------- |
| `Idempotency-Key` is required                                           | `400 idempotency_key_required`      |
| The price on the card stays inside `max_credits_per_request` on the key | `403 request_credit_limit_exceeded` |
| The follow-up run stays inside the daily budget and the monthly budget  | `429`, with a `Retry-After` header  |
| Your workspace has the credits                                          | `402 insufficient_credits`          |

An approval that another caller already decided answers `already_decided`, and one that ran out of time answers `expired`. Neither one charges you. An approval that does not belong to this run answers `404 agent_gate_not_found`.

Poll `follow_up_run_id` as you poll any other run. Each render it starts is an ordinary job. Refer to [Jobs](/concepts/jobs).

<Callout type="warn">
  Approve is a REST operation only. It is not an MCP tool, and it will not become one. A connected model may **decline** an approval with `roasos_agent_decline_approval`. A connected model can never approve its own spend. Refer to [Approving spend is never a tool](/mcp#approving-spend-is-never-a-tool).
</Callout>

## Decline an approval

`POST /v1/agent-runs/{id}/gates/{gate_id}/decline` refuses the work. Nothing starts, and roasOS charges nothing. The body takes an optional `note`. Refer to [Decline an approval](/agent/ask-the-agent#decline-an-approval).

## Cancel a run

```bash
curl -X POST https://api.roasos.com/v1/agent-runs/3d9c17aa-.../cancel \
  -H "Authorization: Bearer roasos_sk_..."
```

The answer is the run envelope. A run that stopped at once reads `canceled`. A run that still works on a step reads `canceling` with a `cancel_requested_at` timestamp. Poll the run until it becomes terminal. roasOS refunds the part of the reservation that the run did not use.

A generation that the run already started keeps its own lifecycle. Stop that job at `POST /v1/jobs/{id}/cancel`.

A run that already finished returns `409 run_not_cancelable`.

<Callout type="warn">
  Cancel is a REST operation only. It is not an MCP tool. A connected model must not stop a run that a person started. (Job cancel is different: `roasos_cancel_job` cancels only a job that the same connection started.)
</Callout>

## Agent runs send no webhook

An agent run has no `webhook_url` field. Poll `GET /v1/agent-runs/{id}/status`.

A generation that a run starts is an ordinary job. That job obeys the usual rules of [Jobs](/concepts/jobs), and it appears in `GET /v1/jobs`.

## Errors

| Status | Code                                                             | Cause                                                                          |
| ------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `400`  | `validation_error`                                               | The run id or the approval id is not a UUID                                    |
| `400`  | `idempotency_key_required`                                       | The `Idempotency-Key` header is absent on an approve                           |
| `400`  | `spend_cap_exceeds_policy`                                       | The caps on the approve body are above the per-request maximum of the key      |
| `400`  | `gate_drop_invalid`                                              | `drop` names an item that the card does not have, or it names all of the items |
| `402`  | `insufficient_credits`                                           | The balance does not cover the approved work                                   |
| `403`  | `request_credit_limit_exceeded`                                  | The approved amount is above the per-request ceiling of the key                |
| `404`  | `agent_run_not_found`                                            | roasOS cannot find the run in your workspace                                   |
| `404`  | `agent_gate_not_found`                                           | The approval does not belong to this run                                       |
| `409`  | `run_not_completed`                                              | `/result` before the run is terminal                                           |
| `409`  | `run_not_cancelable`                                             | The run already finished                                                       |
| `409`  | `agent_gate_not_approvable`                                      | The approval card asks a question, and no render is behind it                  |
| `429`  | `daily_credit_budget_exceeded`, `monthly_credit_budget_exceeded` | The budget of the key has no space                                             |
| `503`  | `agent_run_unavailable`                                          | The run projection is not available at this time                               |
