---
title: Runs and results
description: Poll a schedule run, read its structured output and artifacts, and cancel it.
---

Every fire — cron, manual, or API — produces a run with a receipt you can poll.
Polling always works; [run webhooks](/agents/run-webhooks) deliver the same
receipt without a loop on both `api.dev.sume.com` and `api.sume.com`.

## Run lifecycle

```text
queued -> processing -> completed | failed | canceled | skipped
```

| Status | Meaning |
|---|---|
| `queued` | Accepted, not started. |
| `processing` | The Agent is working. |
| `completed` | Finished. `output` and `artifacts` are populated. |
| `failed` | Finished with an error. |
| `canceled` | Stopped by a cancel request. |
| `skipped` | Never ran, because another run was active. |

The Action vocabulary spells it `canceled` with one `l`, and it is a remapping
of internal statuses — `done` surfaces as `completed`, `error` as `failed`, and
`cancelled` as `canceled`. Do not assume job-side status strings transfer.

## The run receipt

`GET /v1/action-runs/{run_id}` returns the full receipt.

| Field | Notes |
|---|---|
| `id`, `object` | `object` is `action.run`. |
| `action` | `{ "id", "title", "trigger_type" }`. |
| `status` | See the table above. |
| `trigger` | `{ "source": "cron" \| "manual" \| "api", "idempotency_key" }`. |
| `created_at`, `started_at`, `finished_at` | `started_at` and `finished_at` are `null` until they happen. |
| `output_schema` | `{ "name", "strict", "source" }`, where `source` is `default`, `action_default`, or `request_override`. |
| `output` | Structured result. `null` unless `status` is `completed`. |
| `output_error` | `{ "code", "message", "details" }` when projection failed. `null` unless `status` is `completed`. See [When output cannot be produced](/formats/structured-output#when-output-cannot-be-produced). |
| `primary_output_key` | `null` unless `status` is `completed`. |
| `primary_output_url` | Resolved URL for `primary_output_key`. `null` unless `status` is `completed`. |
| `artifacts` | Media harvested from the run. Empty until the run is terminal. |
| `usage` | `{ "currency": "USD", "billable_amount_usd_micros", "generation_spend_cap_usd_micros" }`, or `null` when spend could not be read. |
| `error` | `{ "code": "action_run_failed", "message" }`. Non-null only when `status` is `failed`. |
| `skip_reason` | `previous_run_active` on a skipped run, otherwise `null`. |
| `request_id` | Log this. |
| `status_url`, `result_url`, `cancel_url` | Follow these rather than building URLs. |
| `events_url` | Always `null`. Run lifecycle events are not exposed over the API. |
| `cancelable` | `true` while `queued` or `processing`. |
| `next_action` | Recommended next step — see below. |
| `idempotency_hit` | `true` when this receipt is an idempotency replay. |

`usage.billable_amount_usd_micros` is the **generation** spend attributed to this
run — the same running total the run's own
`generation_spend_cap_usd_micros` is enforced against, counting both reserved
and captured amounts. It climbs while the run is in flight and settles when the
run terminates.

Two things it is not. It excludes the agent's own LLM turn, which bills the
separate Agent wallet, so it is not the run's total cost. And it is a receipt
figure, not an invoice — `GET /v1/usage` remains the authoritative billing
record.

`usage` itself is `null` when spend could not be read at all. That is
distinct from `0`, which means the run really has spent nothing yet.

## Poll for completion

`GET /v1/action-runs/{run_id}/status` returns a trimmed payload built for
polling loops:

```bash
curl -sS "https://api.sume.com/v1/action-runs/$RUN_ID/status" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": {
    "id": "run_...",
    "status": "processing",
    "started_at": "2026-07-31T09:00:01.000Z",
    "finished_at": null,
    "next_action": "poll_status",
    "cancelable": true
  }
}
```

Branch on `next_action`:

| `next_action` | When | Do |
|---|---|---|
| `poll_status` | `queued` or `processing` | Keep polling with backoff. |
| `retry_later` | `skipped` | Another run was active; try again. |
| `none` | Every terminal run — `completed`, `failed`, `canceled` | Nothing left to fetch. On a failure, read `error` and `output_error` on this receipt. |

These three are the whole set. `events_url` is always `null` — there is no
public run events route — so no receipt asks you to inspect one.

## Fetch the result

`GET /v1/action-runs/{run_id}/result` returns the full receipt, but only once
the run is terminal. While the run is `queued` or `processing` it returns
`409 run_not_completed` with the current status in `details.status`.

```bash
curl -sS "https://api.sume.com/v1/action-runs/$RUN_ID/result" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

## Structured output

Schedule runs use the same structured-output contract as Format runs, and it is
documented once, on [Structured output](/formats/structured-output): the
supported schema subset, `SumeMediaFile`, the built-in
`sume/action-run-output/v1` schema, the URL gate, `primary_output_key`
resolution, and the `output_error` failure modes all apply here unchanged.

Two things are Scheduled's own:

- A schedule binds its default schema in the dashboard rather than at author
  time, and that binding shows up on the receipt as
  `output_schema.source: "action_default"`.
- A per-request `output_schema` on `POST /v1/actions/{action_id}/runs` overrides
  it for that run, and shows up as `request_override`.

## List runs

```bash
curl -sS "https://api.sume.com/v1/actions/$ACTION_ID/runs?limit=20" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

`limit` accepts 1–100 and defaults to 50. The response is a page:
`{ "data": [ ... ], "has_more": false, "next_cursor": null }`. Walk the whole
history by passing `next_cursor` back as `cursor` until `has_more` is `false`;
the cursor is opaque, and one we did not mint is rejected with
`400 invalid_request`.
`GET /v1/actions` adds `has_more` and `next_cursor`, but pagination is not
implemented — `has_more` is always `false` and `next_cursor` is always `null`.

A single run is also readable under its Action at
`GET /v1/actions/{action_id}/runs/{run_id}`.

## Cancel a run

```bash
curl -sS -X POST "https://api.sume.com/v1/action-runs/$RUN_ID/cancel" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

Cancel requires `actions:write` and is idempotent. Canceling an
already-terminal run returns that terminal receipt with `200`.

## End-to-end example

```bash
export SUME_API_KEY="sume_live_..."
export ACTION_ID="aut_..."

RUN=$(curl -sS -X POST "https://api.sume.com/v1/actions/$ACTION_ID/runs" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"input":{"product_name":"Aurora Headphones"}}')

RUN_ID=$(echo "$RUN" | jq -r '.data.id')
STATUS=$(echo "$RUN" | jq -r '.data.status')

while [ "$STATUS" = "queued" ] || [ "$STATUS" = "processing" ]; do
  sleep 5
  STATUS=$(curl -sS "https://api.sume.com/v1/action-runs/$RUN_ID/status" \
    -H "Authorization: Bearer $SUME_API_KEY" | jq -r '.data.status')
done

if [ "$STATUS" = "completed" ]; then
  curl -sS "https://api.sume.com/v1/action-runs/$RUN_ID/result" \
    -H "Authorization: Bearer $SUME_API_KEY" \
    | jq -r '.data.primary_output_url'
else
  echo "run ended as $STATUS"
fi
```

Use exponential backoff in production rather than a fixed five-second sleep.

## Next

- [Advanced: run a schedule via API](/agents/actions/api-trigger)
- [Safe automation](/agents/safe-automation)
