Runs
Read an agent run: its status, its approvals, its events, its cost, and how to stop it.
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
{
"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.
| 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.
The events
curl https://api.roasos.com/v1/agent-runs/3d9c17aa-.../events \
-H "Authorization: Bearer roasos_sk_..."{
"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.
{
"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.
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.
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.
{
"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.
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.
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." }'{
"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.
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:
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
dropdo not run, and roasOS does not charge for them. - roasOS checks the per-request ceiling of the key against the items that you keep.
dropmust 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
droplist 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.
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.
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.
Cancel a run
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.
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.)
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, 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 |