---
title: "Ask the agent"
description: "Start one unattended agent run with POST /v1/agent/completions."
url: https://docs.roasos.com/agent/ask-the-agent
---

# Ask the agent

`POST /v1/agent/completions` starts one agent run from free text. The run plans, calls the tools, and can start paid work up to the caps in your request.

The call is quote-first, like every paid call in roasOS. Quote the run, confirm the quote, poll the run, then read the answer.

## 1. Quote

Set `quote_only` to `true`. A quote is free, and it reserves nothing.

Send an `Idempotency-Key` header. Use 16 to 255 characters. Use the same key for the quote and for its confirm.

```bash
curl -X POST https://api.roasos.com/v1/agent/completions \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: glow-serum-ugc-001" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Look at my product Glow Serum. Plan a 15 second UGC ad, then render the first shot.",
    "max_credits": 25,
    "generation_spend_cap_credits": 40,
    "quote_only": true
  }'
```

The body has six fields. Only `text` is required.

| Field                          | Rule                                                                                                                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`                         | Required. 1 to 8,000 characters.                                                                                                                                          |
| `conversation_id`              | Optional. The UUID of a thread to continue. Omit it to start a thread.                                                                                                    |
| `finished_ad`                  | Optional. The default is `false`. Set it to `true` to stitch the generations of the run into one deliverable. Refer to [Assembly](/concepts/assembly).                    |
| `max_credits`                  | Optional. 1 to 100,000. The default is 25, or your key's unattended limit if that limit is lower. The value 0 is refused, because a run with no token budget cannot work. |
| `generation_spend_cap_credits` | Optional. 0 to 100,000. The default is 0.                                                                                                                                 |
| `quote_only` / `quote_id`      | Send one of the two. Never send both.                                                                                                                                     |

The answer is `200`. The status is `quoted`.

```json
{
  "data": {
    "schema_version": "agent-run@1",
    "kind": "agent_run",
    "id": "3d9c17aa-...",
    "trigger": "api",
    "skill_id": null,
    "conversation_id": null,
    "status": "quoted",
    "max_credits": 25,
    "generation_spend_cap_credits": 40,
    "credits_reserved": 0,
    "credits_max": 65,
    "pricing_version": "agent-token-pricing@2026-10-04.v3",
    "quote_id": "b0f4e2c9-...",
    "expires_at": "2026-08-21T10:17:44.000Z",
    "authorization": { "status": "auto_approved" },
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:07:44.000Z"
  }
}
```

Read `credits_max`. It is the **ceiling** of the run: `max_credits + generation_spend_cap_credits`. It is not an estimate of the true cost.

The `id` in a quote is already the id of the run. roasOS makes it from your idempotency key, so the quote and its confirm always name the same run.

A quote is always `auto_approved`. A ceiling that needs a person is refused before a quote exists, with `402 approval_required`. Refer to [Spend caps and approvals](#spend-caps-and-approvals).

Read `expires_at` and confirm before that time. roasOS accepts a quote in the ten-minute window that made it, and in the window after it. A quote that is too old answers `410 quote_expired`. Quote again under a new idempotency key.

## 2. Confirm

Send the same body with the `quote_id`. Use the same idempotency key. Remove `quote_only`.

```bash
curl -X POST https://api.roasos.com/v1/agent/completions \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: glow-serum-ugc-001" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Look at my product Glow Serum. Plan a 15 second UGC ad, then render the first shot.",
    "max_credits": 25,
    "generation_spend_cap_credits": 40,
    "quote_id": "b0f4e2c9-..."
  }'
```

The answer is `202`. roasOS reserves the ceiling and starts the run.

```json
{
  "data": {
    "schema_version": "agent-run@1",
    "kind": "agent_run",
    "id": "3d9c17aa-...",
    "trigger": "api",
    "skill_id": null,
    "conversation_id": null,
    "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": [],
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:08:02.000Z"
  }
}
```

The `id` is the run id. Keep it. Every later read uses it.

A run that does not name a thread makes one. The thread id then appears as `conversation_id` in the run and in the result.

## 3. Poll the run

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

```json
{ "data": { "id": "3d9c17aa-...", "status": "processing", "updated_at": "2026-08-21T10:08:31.000Z" } }
```

The status changes from `processing` to `completed`, `failed`, or `canceled`. Poll every few seconds. Refer to [Runs](/agent/runs).

## 4. Read the result

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

```json
{
  "data": {
    "schema_version": "agent-run-result@1",
    "kind": "agent_run_result",
    "run_id": "3d9c17aa-...",
    "conversation_id": "1f0c8b52-...",
    "trigger": "api",
    "skill_id": null,
    "status": "completed",
    "text": "I analyzed Glow Serum and wrote a three shot script. Shot 1 is rendered.",
    "url": "https://www.roasos.com/home/1f0c8b52-...",
    "completed_at": "2026-08-21T10:11:09.000Z",
    "usage": {
      "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
    }
  }
}
```

The `text` field is the answer of the agent. The `url` field opens the thread in roasOS. The thread holds the cards: the storyboard, the jobs, and the approvals.

<Callout type="warn">
  `/result` returns `409 run_not_completed` while the run is not terminal. This is not an error in your code. Poll `/status` first.
</Callout>

## Spend caps and approvals

The two caps of a run compose with the spend policy of your API key. The policy always wins.

| Condition                                                                               | Answer                             |
| --------------------------------------------------------------------------------------- | ---------------------------------- |
| The ceiling is more than the maximum price for one request of the key                   | `400 spend_cap_exceeds_policy`     |
| The ceiling is more than the unattended limit of the key (`auto_approve_up_to_credits`) | `402 approval_required`            |
| The daily budget or the monthly budget of the key has no space                          | `429`, with a `Retry-After` header |
| Your workspace does not have sufficient credits for the ceiling                         | `402 insufficient_credits`         |

The whole reservation of a run is unattended spend. roasOS measures the **sum** of the two caps against the unattended limit. It does not measure each cap on its own. A key with an unattended limit of 0 credits therefore starts no run at all. Raise the limit of the key, or lower the caps of the request.

Set these limits for each key in your workspace settings. Refer to [Quotes and credits](/concepts/quotes-and-credits).

### What the run approves by itself

The agent starts paid work as a **generation**. Each generation quotes and reserves on its own. The run cap sits above those generations. It never replaces them.

The run approves a generation by itself when both conditions are true:

1. The price is inside the credits that remain in `generation_spend_cap_credits`.
2. The price is inside `auto_approve_up_to_credits` on your key.

The run then writes a `gate_auto_approved` event and continues.

In all other conditions the approval stays open. It appears in `pending_approvals` on the run. A person opens the card in roasOS and approves it there, or your software approves it over REST.

### A storyboard render has three more conditions

A storyboard renders several clips under one card, so it must satisfy the two rules above and three more.

1. roasOS priced every compiled request on the worker. A partial price set never approves itself. One clip would run at the approved price, and another would ask for a new price.
2. The board compiles to 8 requests or fewer. One approval card carries at most eight quotes. Count the requests as shots, multiplied by variants, multiplied by matrix cells.
3. The board does not use the first-frame pipeline. That pipeline stops for a person to approve each frame, which is the reason it exists.

A board that misses one of the five conditions waits in `pending_approvals`. Refer to [Remix an ad](/workflows/remix-an-ad) for the whole loop.

### Approve an approval

An API caller can release an open approval. Use this door for a run that has no thread, such as a skill run or a workflow run.

```bash
curl -X POST https://api.roasos.com/v1/agent-runs/3d9c17aa-.../gates/44b1d0e7-.../approve \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: hook-test-2026-08-21-approve" \
  -H "Content-Type: application/json" \
  -d '{ "generation_spend_cap_credits": 120 }'
```

The answer names the follow-up run in `follow_up_run_id`. An `Idempotency-Key` is required, and the spend policy of the key bounds the amount. Refer to [Approve an approval](/agent/runs#approve-an-approval).

<Callout type="warn">
  Approve is a REST operation only. It is never an MCP tool. A connected model may decline an approval. It must never approve its own spend.
</Callout>

### Storyboard costs and retries

A storyboard's local compile cost is an estimate. A rendering stage starts only after a worker quote fits its saved credit limit. Its approved input stays fixed. First-frame and chained renders enforce this rule for each stage; a separately requested frame regeneration has its own displayed limit.

Retry an uncertain storyboard submission with the same approval. Keep its stored quote and payment key. A previously accepted request can replay after quote expiry; an expired, unsubmitted quote needs a fresh approval. Changes to the saved storyboard invalidate an unconsumed approval for its older version.

### Decline an approval

An API caller can refuse an open approval. The work behind it then never starts, and roasOS charges nothing for it.

```bash
curl -X POST https://api.roasos.com/v1/agent-runs/3d9c17aa-.../gates/44b1d0e7-.../decline \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "note": "Use the other product." }'
```

```json
{
  "data": {
    "gate_id": "44b1d0e7-...",
    "run_id": "3d9c17aa-...",
    "status": "declined",
    "decided_at": "2026-08-21T10:10:02.000Z"
  }
}
```

The `note` field is optional. Its limit is 400 characters.

A decline is final. Approve the same card at [`/approve`](#approve-an-approval) while it is still open. A person can also approve it on its card in roasOS. There is no approve **tool** on MCP, and there will not be one.

## What the run costs

roasOS reserves the ceiling at the confirm step. It settles the run from the tokens that the run truly used. It refunds the rest. You therefore never pay more than the number in the quote.

Refer to [Runs](/agent/runs#what-a-run-costs) for each field of the `usage` object.

## Errors at the door

| Status | Code                                                                            | Cause                                                                                             |
| ------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `400`  | `idempotency_key_required`                                                      | The `Idempotency-Key` header is absent                                                            |
| `400`  | `invalid_idempotency_key`                                                       | The header is not 16 to 255 characters of `A-Z a-z 0-9 . _ : -`                                   |
| `400`  | `validation_error`                                                              | The body is not valid, `max_credits` is 0, or the body holds `quote_only` and `quote_id` together |
| `400`  | `quote_required`                                                                | The body has no `quote_only` value and no `quote_id`                                              |
| `400`  | `spend_cap_exceeds_policy`                                                      | The ceiling is more than the maximum price for one request of the key                             |
| `402`  | `approval_required`                                                             | The ceiling is more than the unattended limit of the key                                          |
| `402`  | `insufficient_credits`                                                          | Your workspace does not have sufficient credits                                                   |
| `403`  | —                                                                               | The key does not have the `api:write` scope                                                       |
| `409`  | —                                                                               | The idempotency key was used again with different data                                            |
| `410`  | `quote_expired`                                                                 | Quote the run again under a new idempotency key                                                   |
| `429`  | `rate_limit_exceeded`                                                           | Too many requests                                                                                 |
| `429`  | `daily_credit_budget_exceeded`, `monthly_credit_budget_exceeded`                | The budget of the key has no space. Read `Retry-After`.                                           |
| `503`  | `agent_runner_unavailable`, `agent_run_unavailable`, `spend_policy_unavailable` | roasOS could not record or start the run                                                          |

A `503` charges you nothing. If a reservation was already made and roasOS could not confirm the outcome, a cron finds the run, fails it, and refunds it.

## From an agent

The same operation is the MCP tool `roasos_agent_ask` on `https://mcp.roasos.com/mcp`. The quote rule is the same, and the tool answers with a run id. The client must then poll `roasos_get_agent_run_status`. Refer to [Connect an AI agent (MCP)](/mcp).

## Next

* Read the run, its events, and its cost: [Runs](/agent/runs).
* Run a saved ask: [Skills](/agent/skills).
