---
title: 실행과 결과
description: Format run receipt를 필드별로 살펴보세요 — 수명주기, 폴링, next_action, usage, 목록, 취소입니다.
---

Format run은 비동기입니다. `POST .../runs`는 즉시 receipt를 건네며, 이후 무슨 일이
있었는지는 그 receipt로 알 수 있습니다.

한 run은 여전히 한 단위의 작업입니다. bulk 요청은 새 실행 엔진이 아니라 그 run들의
서버 측 **큐**입니다 — [대량 실행](/formats/bulk-runs)을 보세요. 큐 진행은
`GET /v1/format-run-queues/{queue_id}`(큐 receipt의 `status_url`)에서 `counts`와
item 상태를 폴링합니다. 각 자식은 이 페이지의 Format-run 엔드포인트를 그대로 씁니다.

폴링은 항상 동작합니다. [Run 웹훅](/agents/run-webhooks)은 루프 없이 같은
receipt를 `api.dev.sume.com`과 `api.sume.com` 모두에서 밀어 줍니다.

정확한 요청·응답 스키마는 라이브 OpenAPI
(`https://api.sume.com/reference/json`)에서 가져옵니다. 여기 표는 읽기 쉬운 요약이며,
두 번째 스키마가 아닙니다.

## 수명주기

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

| Status | Meaning |
|---|---|
| `queued` | 수락되었고, 아직 시작되지 않았습니다. |
| `processing` | run이 작업 중입니다. |
| `completed` | 끝났습니다. `output`과 `artifacts`가 채워집니다. |
| `failed` | 오류와 함께 끝났습니다. |
| `canceled` | 취소 요청으로 멈췄습니다. |
| `skipped` | 이미 진행 중인 run이 있어 실행되지 않았습니다. |

`canceled`는 `l`이 하나인 철자입니다. 이것은 API 자체의 상태 이름이며 내부 job
상태가 아닙니다 — job 쪽 문자열이 그대로 온다고 가정하지 마세요.

## Bulk 큐

`POST /v1/formats/{format_id}/bulk-runs`(및 vanity 쌍)는 큐 객체
(`object: "format.run_queue"`, id `frq_…`)를 반환합니다. 그 목록의 진행은 Format-run
폴링이 아니라 `GET /v1/format-run-queues/{queue_id}`입니다. 큐 `status`가
`completed`이면 모든 item이 종료입니다. `counts.failed` / `counts.canceled`를
보세요. 자식 receipt는 `GET /v1/format-runs/{run_id}`에 있습니다. 전체 계약:
[대량 실행](/formats/bulk-runs).

## 읽기 엔드포인트 세 개

receipt는 자체 URL을 담습니다. 경로를 직접 만들지 말고 그 URL을 따르세요.

| Field | Endpoint | Returns |
|---|---|---|
| `status_url` | `GET /v1/format-runs/{run_id}/status` | `id`, `status`, `started_at`, `finished_at`, `next_action`, `cancelable` — 가벼운 폴링입니다. |
| `result_url` | `GET /v1/format-runs/{run_id}/result` | 전체 receipt. 진행 중이면 `details.status`에 현재 상태와 함께 `409 run_not_completed`입니다. |
| `cancel_url` | `POST /v1/format-runs/{run_id}/cancel` | 현재 receipt입니다. |
| — | `GET /v1/format-runs/{run_id}` | 어떤 상태에서든 전체 receipt입니다. |

```bash
curl -sS "https://api.sume.com/v1/format-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
  }
}
```

## Run receipt

`GET /v1/format-runs/{run_id}`는 전체 형태를 반환합니다.

| Field | Notes |
|---|---|
| `id`, `object` | `object`는 `format.run`입니다. |
| `format` | `{ "id", "slug", "title", "version" }`. `id`는 불투명한 `skl_…`입니다. |
| `status` | 위 표를 참고하세요. |
| `trigger` | `{ "source": "cron" \| "manual" \| "api", "idempotency_key" }`. API run은 `api`입니다. |
| `created_at`, `started_at`, `finished_at` | 뒤의 둘은 해당 시점까지 `null`입니다. |
| `output_schema` | `{ "name", "strict", "source" }` — `output`을 만든 스키마입니다. [구조화 출력](/formats/structured-output#bind-a-schema)을 참고하세요. |
| `output` | 구조화된 결과입니다. 종료가 아닌 모든 상태와 `output_error`가 설정된 경우에는 `null`입니다. |
| `output_error` | 투영이 `output`을 만들지 못했을 때 `{ "code", "message", "details" }`입니다. [출력을 만들 수 없을 때](/formats/structured-output#when-output-cannot-be-produced)를 참고하세요. |
| `primary_output_key` | `output`에서 보여줄 하나의 URL에 해당하는 키입니다. `completed`가 아니면 `null`입니다. |
| `primary_output_url` | `primary_output_key`의 해석된 URL입니다. `completed`가 아니면 `null`입니다. |
| `artifacts` | run이 만든 모든 내구성 파일입니다. 종료될 때까지 비어 있습니다. |
| `usage` | `{ "currency": "USD", "billable_amount_usd_micros", "generation_spend_cap_usd_micros" }`이거나, spend를 읽지 못했을 때 `null`입니다. |
| `error` | `{ "code", "message" }`. `status`가 `failed`일 때만 non-null입니다. |
| `skip_reason` | `skipped` run에서 채워지며, 그 외에는 `null`입니다. |
| `request_id` | 로그에 남기세요. 지원팀이 요청하는 값입니다. |
| `status_url`, `result_url`, `cancel_url` | 위를 참고하세요. |
| `events_url` | 이 run의 phase 타임라인 — 무엇을, 언제부터 하고 있는지. Format run에서는 non-null이고, Action·Agent Completion receipt는 여전히 `null`입니다. |
| `cancelable` | `queued` 또는 `processing`인 동안 `true`입니다. |
| `next_action` | 권장하는 다음 단계입니다 — 아래를 참고하세요. |
| `webhook_delivery` | 등록한 콜백의 전달 상태입니다. 등록하지 않았다면 `null`입니다 — 아래를 참고하세요. |
| `idempotency_hit` | 이 receipt가 새 run이 아니라 멱등성 재전송일 때 `true`입니다. |

`artifacts[]`는 `output`을 채우는 같은 job 원장에서 가져오므로 둘은 항상 일치합니다.
미디어 필드는 만료되지 않는 내구성 있는 `media.sume.com` HTTPS URL입니다.

## `next_action`으로 분기하기

Receipt가 내보내는 값은 아래 세 가지뿐입니다:

| `next_action` | When | Do |
|---|---|---|
| `poll_status` | `queued` 또는 `processing` | 백오프로 계속 폴링하세요. |
| `retry_later` | `skipped` | Format 단일 실행(`on_active_run: "skip"`)을 요청했는데 다른 run이 활성이었습니다. 다시 시도하세요. |
| `none` | 종료된 모든 run — `completed`, `failed`, `canceled` | 더 가져올 것이 없습니다. 실패한 경우 사유는 이미 이 receipt의 `error`와 `output_error`에 있습니다. |

Format run receipt에서 `fix_input`, `contact_support`, `inspect_events`는
기대하지 마세요. 앞의 둘은 generation job 표면의 값입니다. `inspect_events`는
공개 events 경로가 없던 시절에 없어진 값이고, 그대로 두었습니다 — 이제
`events_url`로 phase 타임라인을 읽을 수 있지만, 종료된 run에서 필요한 답은 이미
손에 든 receipt에 있습니다.

## 웹훅 전달 확인하기

`communication.webhook_url`로 run을 만들었다면 모든 receipt에 그 콜백이 어떻게 됐는지
알려 주는 `webhook_delivery` 블록이 들어 있습니다. 등록하지 않고 만든 run에서는
`null`입니다.

```json
{
  "webhook_delivery": {
    "url": "https://partner.example/hooks/sume",
    "event_type": "format.run.terminal",
    "status": "delivered",
    "attempts": 1,
    "max_attempts": 10,
    "next_attempt_at": null,
    "last_attempt_at": "2026-08-03T09:00:04.000Z",
    "delivered_at": "2026-08-03T09:00:04.000Z",
    "last_status_code": 200,
    "last_error": null,
    "signature_version": "sume-v1"
  }
}
```

| `status` | 의미 |
|---|---|
| `not_armed` | URL은 저장됐지만 예약된 것이 없습니다 — run이 아직 종료되지 않았거나, 이 환경에서 run 웹훅 전달이 꺼져 있습니다. |
| `pending` | 예약됐고 첫 시도를 기다립니다. |
| `retrying` | 시도가 실패했습니다. `next_attempt_at`이 다음 시도 시각입니다. HTTP 429는 exponential backoff 위에 `Retry-After`를 존중합니다(최대 1시간). |
| `delivered` | 엔드포인트가 2xx를 반환했습니다. |
| `failed` / `exhausted` | 포기했습니다(10회). `last_status_code`와 `last_error`를 읽으세요. 여기 429는 수신함 rate limit입니다. run 자체는 그대로이니 `result_url`을 폴링하세요. |

`not_armed`는 URL은 저장됐지만 아직 전송이 예약되지 않은 상태입니다(대개 run이
아직 종료되지 않음). 종료 후에는 보통 `pending` → `delivered`(또는 재시도/실패
상태)로 움직입니다. 폴링은 백업으로 계속 쓸 수 있습니다.

`last_error`는 우리 쪽 전송 오류(타임아웃, 연결 실패, 2xx가 아닌 상태)이며 여러분의
응답 본문이 아닙니다.

## `usage` 읽기

`usage.billable_amount_usd_micros`는 이 run에 귀속된 **generation** spend입니다 —
run의 `generation_spend_cap_usd_micros`가 강제되는 같은 누적 합이며, 예약분과 확정분을
모두 셉니다. run이 진행되는 동안 올라가고, 종료되면 정착합니다.

아닌 것이 두 가지입니다. Agent 자체의 LLM 턴은 별도 지갑으로 청구되므로 제외되며,
run의 총비용이 아닙니다. 또한 receipt 숫자이지 청구서가 아닙니다 —
[`GET /v1/usage`](/dashboard/usage)가 권위 있는 빌링 기록입니다.

`usage` 자체가 `null`인 경우는 spend를 전혀 읽지 못했을 때입니다. 아직 정말 아무것도
쓰지 않았다는 뜻인 `0`과는 다릅니다.

## 어떤 버전이 실행됐는지

`format.version`은 그 run을 만든 Format 버전을 기록합니다. Format을 수정하면 버전이
올라가고 이미 발급된 receipt는 건드리지 않으므로, 과거 run은 실제로 실행한 버전을
항상 알려 줍니다.

## Format의 run 목록

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

최신이 먼저입니다. `limit`은 1–100을 받으며 기본값은 20입니다. API 트리거를 한 번도
쓰지 않은 Format은 404가 아니라 빈 목록을 돌려줍니다.

응답은 한 페이지입니다:

```json
{
  "data": [{ "id": "arun_...", "object": "format.run" }],
  "has_more": true,
  "next_cursor": "MjAyNi0wOC0wM1QwOTowMDowMC4wMDBafGFydW5fMQ"
}
```

전체 이력을 훑으려면 `has_more`가 `false`가 될 때까지 `next_cursor`를 `cursor`로
다시 넘기세요:

```bash
curl -sS "https://api.sume.com/v1/formats/$FORMAT_ID/runs?limit=20&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

cursor는 불투명한 값입니다 — 그대로 다시 넘기고, 파싱하거나 직접 만들지 마세요.
`(created_at, id)` 기준 keyset이므로 페이지를 넘기는 도중 새 run이 생겨도 이미 읽은
페이지로 행이 밀려들지 않습니다. 우리가 발급하지 않은 cursor는 조용히 최신 run부터
다시 시작하지 않고 `400 invalid_request`로 거절합니다.

vanity 경로도 같습니다: `GET /v1/formats/{handle}/{slug}/runs`.

## 취소

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

`formats:write`가 필요하며 멱등합니다. 이미 종료 상태에 도달한 run을 취소하는 것은
no-op이며, 어느 쪽이든 현재 receipt가 돌아옵니다.

## 끝까지

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

RUN=$(curl -sS -X POST "https://api.sume.com/v1/formats/chase/product-promo/runs" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"input":{"product_url":"https://example.com/p"}}')

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/format-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/format-runs/$RUN_ID/result" \
    -H "Authorization: Bearer $SUME_API_KEY" \
    | jq -r '.data.primary_output_url'
else
  echo "run ended as $STATUS"
fi
```

프로덕션에서는 고정 5초 sleep 대신 exponential backoff를 쓰세요. 비디오를 만드는
run은 분 단위 작업입니다. 매초 폴링해도 이득이 없고 rate limit만 소모합니다.

## 다음

- [구조화 출력](/formats/structured-output) — `output` 형태와 null일 때 할 일
- [Format 호출하기](/formats/call) — invoke 계약과 모든 제출 오류
- [대량 실행](/formats/bulk-runs) — 이 run들의 큐, 그리고 `GET /v1/format-run-queues/{id}`
- [Run 웹훅](/agents/run-webhooks) — 폴링 대신 밀어 주는 종료 receipt
