---
title: 실행과 Job 기다리기
description: 생성 job은 SDK가 기다려 줍니다. Format·Action·Agent 실행은 이 페이지의 HTTP 루프로 기다립니다. 프로덕션에서는 웹훅을 권장합니다.
---

Sume의 비동기 작업은 두 종류이고, URL도 두 갈래입니다.

| 종류 | 만드는 곳 | 읽는 곳 | 기다리는 방법 |
|---|---|---|---|
| **Job** | Image Router, Video Router 등 생성 경로 | `/v1/jobs/{id}` | [`@sume-com/client` 또는 `sume-client`](/sdk)의 `subscribe` |
| **실행(run)** | Format, Action, Agent Completion | `/v1/format-runs/{id}`, `/v1/action-runs/{id}`, `/v1/agent-runs/{id}` | 아래 HTTP 루프 또는 [run 웹훅](/agents/run-webhooks) |

job id는 run id가 아니고, run id도 job id가 아닙니다. 지금은 **SSE 이벤트
스트림이 없습니다.** 기다린다는 것은 상태가 종료될 때까지 다시 읽는다는 뜻입니다.

## 생성 job: `subscribe`

`subscribe`는 job을 제출하고 기다립니다.

```ts
import { createSumeClient } from "@sume-com/client";

const sume = createSumeClient({
  baseURL: "https://api.sume.com",
  maxPollTime: 20 * 60_000,
});

const response = await sume.subscribe("/v1/video-router/generate", {
  input: {
    model: "seedance-2.5",
    prompt: "Slow push-in on a ceramic mug",
    duration: 5,
    resolution: "480p",
  },
});

console.log(response.status, response.video?.url);
```

```python
import sume_client

sume = sume_client.SyncClient(base_url="https://api.sume.com")

result = sume.subscribe(
    "seedance-2.5",
    arguments={
        "prompt": "Slow push-in on a ceramic mug",
        "duration": 5,
        "resolution": "480p",
    },
)
```

- 둘 다 2초마다 `/v1/jobs/{id}/status`를 폴링하고, job이 완료되면
  `/v1/jobs/{id}/result`를 읽습니다.
- TypeScript 클라이언트는 `maxPollTime`(지정하지 않으면 5분)이 지나면 멈추고
  `TimeoutError`를 던집니다. 그 `requestId`가 job id이며, job은 계속 실행되고
  과금도 됩니다. 예상하는 가장 긴 job에 맞춰 `maxPollTime`을 정하세요. 비디오는
  보통 몇 분 걸립니다.
- Python 클라이언트는 job이 끝날 때까지 기다립니다.

### 이미 job id가 있을 때

```python
status = sume.status("job_7513b52d-3669-4ffe-b317-8b02000715eb")
result = sume.result("job_7513b52d-3669-4ffe-b317-8b02000715eb")
sume.cancel("job_7513b52d-3669-4ffe-b317-8b02000715eb")
```

`result()`는 `subscribe`처럼 job이 끝날 때까지 기다립니다. TypeScript에서는 HTTP로
읽으세요. `terminal`이 `true`가 될 때까지 `GET /v1/jobs/{id}/status`, 그다음
`GET /v1/jobs/{id}/result`입니다. 필드와 루프는 [Job과 결과](/workflows/jobs-and-results)에
있습니다.

실패하거나 취소된 job의 `/result`는 `409 job_not_completed`를 답합니다. 그럴 때는
`/v1/jobs/{id}`에서 `status`와 `error`를 읽으세요.

## Format·Action·Agent 실행

SDK에는 실행 호출이 없습니다. `POST` 한 번으로 실행을 만들고, 상태가 종료될
때까지 영수증을 다시 읽으세요.

```ts
const API = "https://api.sume.com";
const headers = {
  Authorization: `Bearer ${process.env.SUME_API_KEY}`,
  "Content-Type": "application/json",
};

const created = await fetch(`${API}/v1/formats/acme/product-promo/runs`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "order-8823-promo-v1" },
  body: JSON.stringify({
    input: { product_url: "https://shop.example.com/p/8823" },
    generation_spend_cap_credits: 300,
  }),
});
if (!created.ok) throw new Error(JSON.stringify(await created.json()));
let run = (await created.json()).data;

let wait = 5_000;
while (run.status === "queued" || run.status === "processing") {
  await new Promise((resolve) => setTimeout(resolve, wait));
  wait = Math.min(wait * 2, 60_000);
  const read = await fetch(`${API}/v1/format-runs/${run.id}`, { headers });
  if (read.status === 429 || read.status >= 500) continue;
  if (!read.ok) throw new Error(JSON.stringify(await read.json()));
  run = (await read.json()).data;
}

console.log(run.status, run.primary_output_url, run.error);
```

```python
import os
import time

import httpx

API = "https://api.sume.com"
headers = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

created = httpx.post(
    f"{API}/v1/formats/acme/product-promo/runs",
    headers={**headers, "Idempotency-Key": "order-8823-promo-v1"},
    json={
        "input": {"product_url": "https://shop.example.com/p/8823"},
        "generation_spend_cap_credits": 300,
    },
)
created.raise_for_status()
run = created.json()["data"]

wait = 5
while run["status"] in ("queued", "processing"):
    time.sleep(wait)
    wait = min(wait * 2, 60)
    read = httpx.get(f"{API}/v1/format-runs/{run['id']}", headers=headers)
    if read.status_code == 429 or read.status_code >= 500:
        continue
    read.raise_for_status()
    run = read.json()["data"]

print(run["status"], run.get("primary_output_url"), run.get("error"))
```

Action과 Agent Completion 실행도 `/v1/action-runs/{id}`, `/v1/agent-runs/{id}`에서
같은 루프를 씁니다. 영수증의 각 필드는 [실행과 결과](/formats/runs)에 있습니다.

루프를 올바르게 유지하는 규칙입니다.

- **생성 요청에 `Idempotency-Key`를 보내세요.** 같은 키로 다시 보낸 생성은 같은
  실행을 돌려줍니다. 키가 없으면 재시도가 두 번째 실행을 시작하고 과금합니다.
- **간격을 늘리세요.** 긴 비디오는 15~30분 걸립니다. 간격을 1분까지 두 배씩
  늘리세요.
- **읽기에서 받은 `429`나 `5xx`는 실행이 아니라 읽기가 실패한 것입니다.** 실행은
  계속 진행되고 과금됩니다. 기다렸다가 다시 읽으세요.
- **`expires_at`을 상한으로 쓰세요.** 종료되지 않은 영수증에는 Sume가 실행을
  `failed`로 마감하는 시각이 있습니다.
- **루프를 멈춰도 실행은 취소되지 않습니다.** run id를 저장해 두고 나중에 읽거나,
  `POST /v1/format-runs/{id}/cancel`로 취소하세요.

## 기다리는 동안 단계 보기

`status`는 Format 실행이 `processing`이라는 것만 알려 줍니다. **단계 타임라인**은
무엇을 하고 있는지 알려 줍니다. `preparing`, `running`, `finalizing`이 각각 시각과
상태와 함께 옵니다. 영수증의 `events_url`을 읽으세요.

```ts
const events = await fetch(run.events_url, { headers });
const { data } = await events.json();
```

로그 스트림이 아니라 단계 타임라인입니다. Sume는 에이전트 출력, 도구 호출, 샌드박스
내부를 공개하지 않습니다. [Run receipt](/formats/runs#run-receipt)의 `events_url`을
참고하세요. Action과 Agent 실행은 `events_url: null`을 보고합니다.

## 종료가 곧 성공은 아닙니다

실행은 `completed`, `failed`, `canceled`, `skipped`에서 종료됩니다. 실패한 실행은
예외가 아니라 요청한 결과입니다. 웹훅 핸들러처럼 영수증의 `status`와 `error`를
읽으세요.

```ts
switch (run.status) {
  case "completed":
    return attachOutputs(run);
  case "failed":
    return showError(run.error);
  case "canceled":
  case "skipped":
    return noop();
}
```

`skipped`는 다른 실행이 진행 중이었고 `on_active_run: "skip"`을 보냈다는 뜻입니다.
Format 실행은 기본적으로 동시 실행을 허용합니다.

## 가능하면 웹훅을

폴링 루프는 진행 중인 실행마다 타이머 하나와 열린 요청 하나를 붙잡고, 실행은 보통
몇 분 걸립니다. [run 웹훅](/agents/run-webhooks)은 같은 영수증을 그 비용 없이
전달하고, [웹훅 검증](/sdk/webhooks)이 받는 쪽의 절반입니다. 루프는 대비책으로
남겨 두세요.

## 다음

- [웹훅 검증](/sdk/webhooks) — 푸시 경로의 받는 쪽 확인
- [실행과 결과](/formats/runs) — 영수증의 각 필드
- [Bulk 실행](/formats/bulk-runs) — 여러 실행을 큐에 넣고 `GET /v1/format-run-queues/{id}`를 폴링
- [제품에 Format 임베드](/cookbooks/embed-a-format) — 파트너 통합 전체
