# Monitor, cancel, and retry workflow runs

> Follow a workflow run to terminal status, cancel active work, retry eligible failures, and reconcile final usage.

Canonical: https://entirefeed.com/docs/workflows/monitor-cancel-retry

Use the public `wrun_...` identifier for every lifecycle request. Poll while a run is `queued` or `running`; stop automatic polling at `succeeded`, `failed`, `cancelled`, or `manual_review`.

Webhooks can replace frequent polling for state changes, but the run resource remains the current source of truth. Final `usage` and billing ledger entries show the settled or released amount for the run.

Read [Errors and review](/docs/operate/errors-and-manual-review) before automating retry behavior.

## Poll to a terminal state

Poll `links.self` while status is `queued` or `running`. Stop at `succeeded`, `failed`, `cancelled`, `manual_review`:

### cURL

```bash
set -euo pipefail

API_URL="https://api.entirefeed.com/v1"
INPUTS='{"video_idea":"A slow product reveal with soft daylight"}'

STARTERS=$(curl -fsS "$API_URL/workflow-starters" \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY")
STARTER_VERSION=$(printf '%s' "$STARTERS" | jq -r   '.data[] | select(.slug == "text-to-video-basic") | .version')

ESTIMATE=$(curl -fsS "$API_URL/workflow-starters/text-to-video-basic/estimate" \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --argjson inputs "$INPUTS" --arg version "$STARTER_VERSION"     '{inputs: $inputs, starter_version: $version}')")
if [ "$(printf '%s' "$ESTIMATE" | jq '.unknown_costs | length')" -ne 0 ]; then
  printf '%s\n' "Estimate contains unknown costs; run creation stopped." >&2
  exit 1
fi
ESTIMATE_VERSION=$(printf '%s' "$ESTIMATE" | jq -r '.estimate_version')

RUN=$(curl -fsS "$API_URL/workflow-starters/text-to-video-basic/runs" \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY" \
  -H "Idempotency-Key: launch-video-042" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --argjson inputs "$INPUTS" --arg starter "$STARTER_VERSION"     --arg estimate "$ESTIMATE_VERSION"     '{inputs: $inputs, starter_version: $starter, estimate_version: $estimate}')")
RUN_ID=$(printf '%s' "$RUN" | jq -r '.id')

while :; do
  RUN=$(curl -fsS "$API_URL/workflow-runs/$RUN_ID" \
    -H "Authorization: Bearer $ENTIREFEED_API_KEY")
  STATUS=$(printf '%s' "$RUN" | jq -r '.status')
  case "$STATUS" in succeeded|failed|cancelled|manual_review) break ;; esac
  sleep 2
done

printf '%s' "$RUN" | jq '{status, output, usage}'
```

### TypeScript

```typescript
const apiUrl = "https://api.entirefeed.com/v1";
const terminal = new Set(["succeeded","failed","cancelled","manual_review"]);
const baseHeaders = {
  Authorization: `Bearer ${process.env.ENTIREFEED_API_KEY}`,
  "Content-Type": "application/json",
};

type Starter = { slug: string; version: string };
type Estimate = { estimate_version: string; unknown_costs: string[] };
type Run = { id: string; status: string; output: unknown; usage: unknown };

async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(`${apiUrl}${path}`, {
    ...init,
    headers: { ...baseHeaders, ...init.headers },
  });
  if (!response.ok) throw new Error(await response.text());
  return (await response.json()) as T;
}

const { data } = await api<{ data: Starter[] }>("/workflow-starters");
const starter = data.find(({ slug }) => slug === "text-to-video-basic");
if (!starter) throw new Error("Starter is not currently published");

const inputs = { video_idea: "A slow product reveal with soft daylight" };
const estimate = await api<Estimate>(
  "/workflow-starters/text-to-video-basic/estimate",
  { method: "POST", body: JSON.stringify({ inputs, starter_version: starter.version }) },
);
if (estimate.unknown_costs.length) throw new Error("Cost cannot be determined");

let run = await api<Run>("/workflow-starters/text-to-video-basic/runs", {
  method: "POST",
  headers: { "Idempotency-Key": "launch-video-042" },
  body: JSON.stringify({
    inputs,
    starter_version: starter.version,
    estimate_version: estimate.estimate_version,
  }),
});

while (!terminal.has(run.status)) {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  run = await api<Run>(`/workflow-runs/${run.id}`);
}
console.log({ status: run.status, output: run.output, usage: run.usage });
```

### Python

```python
import os
import time

import requests

API_URL = "https://api.entirefeed.com/v1"
TERMINAL = set(['succeeded','failed','cancelled','manual_review'])
HEADERS = {"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}"}

starters_response = requests.get(
    f"{API_URL}/workflow-starters", headers=HEADERS, timeout=30
)
starters_response.raise_for_status()
starters = starters_response.json()
starter = next(item for item in starters["data"] if item["slug"] == "text-to-video-basic")
inputs = {"video_idea": "A slow product reveal with soft daylight"}

estimate_response = requests.post(
    f"{API_URL}/workflow-starters/text-to-video-basic/estimate",
    headers=HEADERS,
    json={"inputs": inputs, "starter_version": starter["version"]},
    timeout=30,
)
estimate_response.raise_for_status()
estimate = estimate_response.json()
if estimate["unknown_costs"]:
    raise RuntimeError("Cost cannot be determined")

run_response = requests.post(
    f"{API_URL}/workflow-starters/text-to-video-basic/runs",
    headers={**HEADERS, "Idempotency-Key": "launch-video-042"},
    json={
        "inputs": inputs,
        "starter_version": starter["version"],
        "estimate_version": estimate["estimate_version"],
    },
    timeout=30,
)
run_response.raise_for_status()
run = run_response.json()

while run["status"] not in TERMINAL:
    time.sleep(2)
    run_response = requests.get(
        f"{API_URL}/workflow-runs/{run['id']}", headers=HEADERS, timeout=30
    )
    run_response.raise_for_status()
    run = run_response.json()

print({"status": run["status"], "output": run.get("output"), "usage": run["usage"]})
```

```json
{
  "id": "wrun_0123456789abcdef0123456789abcdef",
  "object": "workflow_run",
  "status": "succeeded",
  "starter_slug": "text-to-video-basic",
  "starter_version": "st_0123456789abcdef01234567",
  "created_at": "2026-07-14T08:30:00Z",
  "started_at": "2026-07-14T08:30:03Z",
  "completed_at": "2026-07-14T08:31:20Z",
  "output": {
    "video": {
      "id": "asset_0123456789abcdef0123456789abcdef",
      "type": "video",
      "url": "https://cdn.entirefeed.com/examples/launch-video.mp4",
      "mime_type": "video/mp4",
      "expires_at": null
    }
  },
  "usage": {
    "amount_usd": "1.47",
    "line_items": [
      {
        "label": "Video generation",
        "amount_usd": "1.47",
        "confidence": "exact",
        "reason": "Final cost confirmed after completion."
      }
    ],
    "billing_status": "settled",
    "billing_reason": "final_cost_confirmed"
  },
  "error": null,
  "metadata": {
    "customer_job_id": "launch-video-042"
  },
  "links": {
    "self": "https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef"
  }
}
```

## Cancel active work

Cancel queued or running work. Cancellation is a request, so read the returned run and continue polling if it has not reached `cancelled`:

### cURL

```bash
curl https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef/cancel \
  -X POST \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Campaign was paused"
}'
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef/cancel", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "Campaign was paused"
  }),
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

### Python

```python
import os
import requests

response = requests.request(
    "POST",
    "https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef/cancel",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}", "Content-Type": "application/json"},
    json={
      "reason": "Campaign was paused",
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

## Retry eligible terminal work

Retry only the latest failed or cancelled run after work started and when its retry lineage has no active run. A retry is new work with a new public ID and requires its own `Idempotency-Key`:

### cURL

```bash
curl https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef/retry \
  -X POST \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY" \
  -H "Idempotency-Key: launch-video-042-retry-1"
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef/retry", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
    "Idempotency-Key": "launch-video-042-retry-1",
  },
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

### Python

```python
import os
import requests

response = requests.request(
    "POST",
    "https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef/retry",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}", "Idempotency-Key": "launch-video-042-retry-1"},
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

```json
{
  "id": "wrun_fedcba9876543210fedcba9876543210",
  "object": "workflow_run",
  "status": "queued",
  "starter_slug": "text-to-video-basic",
  "starter_version": "st_0123456789abcdef01234567",
  "created_at": "2026-07-14T08:35:00Z",
  "estimated_cost": {
    "amount_usd": "1.53"
  },
  "metadata": {
    "customer_job_id": "launch-video-042"
  },
  "links": {
    "self": "https://api.entirefeed.com/v1/workflow-runs/wrun_fedcba9876543210fedcba9876543210"
  }
}
```

After an uncertain response, replay the same source run and key to recover the original retry. Reusing that key for another source returns `idempotency_conflict`. If a newer retry exists, operate on the latest run instead.

## Receive signed run events

Register the run events you need. The signing secret is returned once:

### cURL

```bash
curl https://api.entirefeed.com/v1/webhooks/endpoints \
  -X POST \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/webhooks/entirefeed",
  "events": [
    "workflow_run.succeeded",
    "workflow_run.failed",
    "workflow_run.cancelled",
    "workflow_run.manual_review"
  ]
}'
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/webhooks/endpoints", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://example.com/webhooks/entirefeed",
    "events": [
      "workflow_run.succeeded",
      "workflow_run.failed",
      "workflow_run.cancelled",
      "workflow_run.manual_review"
    ]
  }),
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

### Python

```python
import os
import requests

response = requests.request(
    "POST",
    "https://api.entirefeed.com/v1/webhooks/endpoints",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}", "Content-Type": "application/json"},
    json={
      "url": "https://example.com/webhooks/entirefeed",
      "events": [
        "workflow_run.succeeded",
        "workflow_run.failed",
        "workflow_run.cancelled",
        "workflow_run.manual_review",
      ],
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

Supported events are `workflow_run.queued`, `workflow_run.running`, `workflow_run.succeeded`, `workflow_run.failed`, `workflow_run.cancelled`, `workflow_run.manual_review`. Omitting `events` uses the endpoint defaults: `generation.succeeded`, `generation.failed`, `workflow_run.succeeded`, `workflow_run.failed`.

```json
{
  "id": "f419d0cf-5d8e-4b4c-82bb-6042b17e68f8",
  "url": "https://example.com/webhooks/entirefeed",
  "events": [
    "workflow_run.succeeded",
    "workflow_run.failed",
    "workflow_run.cancelled",
    "workflow_run.manual_review"
  ],
  "secret": "whsec_store_this_value_once"
}
```

Verify `EntireFeed-Signature` against the exact raw body before parsing, store `EntireFeed-Event-ID`, and return `2xx` after durable receipt:

### TypeScript

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const parts = Object.fromEntries(
  signatureHeader.split(",").map((part) => part.split("=", 2)),
);
const timestamp = Number(parts.t);
const received = Buffer.from(parts.v1 ?? "", "hex");
const expected = Buffer.from(
  createHmac("sha256", process.env.ENTIREFEED_WEBHOOK_SECRET!)
    .update(Buffer.concat([Buffer.from(String(timestamp) + "."), rawBody]))
    .digest("hex"),
  "hex",
);

const fresh = Math.abs(Date.now() / 1000 - timestamp) <= 300;
const valid =
  fresh &&
  received.length === expected.length &&
  timingSafeEqual(received, expected);
```

### Python

```python
import hashlib
import hmac
import os
import time

parts = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = int(parts["t"])
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
    os.environ["ENTIREFEED_WEBHOOK_SECRET"].encode(),
    signed,
    hashlib.sha256,
).hexdigest()

fresh = abs(time.time() - timestamp) <= 300
valid = fresh and hmac.compare_digest(parts["v1"], expected)
```

```json
{
  "id": "evt_0123456789abcdef0123456789abcdef",
  "type": "workflow_run.succeeded",
  "created_at": "2026-07-14T08:31:20Z",
  "data": {
    "object": {
      "id": "wrun_0123456789abcdef0123456789abcdef",
      "object": "workflow_run",
      "status": "succeeded",
      "starter_slug": "text-to-video-basic",
      "starter_version": "st_0123456789abcdef01234567",
      "created_at": "2026-07-14T08:30:00Z",
      "started_at": "2026-07-14T08:30:03Z",
      "completed_at": "2026-07-14T08:31:20Z",
      "output": {
        "video": {
          "id": "asset_0123456789abcdef0123456789abcdef",
          "type": "video",
          "url": "https://cdn.entirefeed.com/examples/launch-video.mp4",
          "mime_type": "video/mp4",
          "expires_at": null
        }
      },
      "usage": {
        "amount_usd": "1.47",
        "line_items": [
          {
            "label": "Video generation",
            "amount_usd": "1.47",
            "confidence": "exact",
            "reason": "Final cost confirmed after completion."
          }
        ],
        "billing_status": "settled",
        "billing_reason": "final_cost_confirmed"
      },
      "error": null,
      "metadata": {
        "customer_job_id": "launch-video-042"
      },
      "links": {
        "self": "https://api.entirefeed.com/v1/workflow-runs/wrun_0123456789abcdef0123456789abcdef"
      }
    }
  }
}
```

## Reconcile final usage

The terminal run's `usage` is the run-level billing outcome. Filter the ledger by `workflow_run_id` to reconcile reservation, settlement, and release entries:

### cURL

```bash
curl https://api.entirefeed.com/v1/billing/ledger?workflow_run_id=wrun_0123456789abcdef0123456789abcdef \
  -X GET \
  -H "Authorization: Bearer $ENTIREFEED_API_KEY"
```

### TypeScript

```typescript
const response = await fetch("https://api.entirefeed.com/v1/billing/ledger?workflow_run_id=wrun_0123456789abcdef0123456789abcdef", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.ENTIREFEED_API_KEY}`,
  },
});

if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

### Python

```python
import os
import requests

response = requests.request(
    "GET",
    "https://api.entirefeed.com/v1/billing/ledger?workflow_run_id=wrun_0123456789abcdef0123456789abcdef",
    headers={"Authorization": f"Bearer {os.environ['ENTIREFEED_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

```json
{
  "data": [
    {
      "id": "2f55ef4d-1f48-45db-9406-23754a3287ba",
      "type": "RESERVE",
      "amount_usd": "-1.53",
      "available_after_usd": "8.47",
      "reserved_after_usd": "1.53",
      "workflow_run_id": "wrun_0123456789abcdef0123456789abcdef",
      "description": "Reserved estimated operation cost.",
      "created_at": "2026-07-14T08:30:00Z"
    },
    {
      "id": "923fce22-42b1-4c74-8dc7-fafaf8fd64c4",
      "type": "SETTLE",
      "amount_usd": "-1.47",
      "available_after_usd": "8.47",
      "reserved_after_usd": "0.06",
      "workflow_run_id": "wrun_0123456789abcdef0123456789abcdef",
      "description": "Settled final operation cost.",
      "created_at": "2026-07-14T08:31:20Z"
    },
    {
      "id": "f0d251ef-a44f-41c3-b2f8-9d8db0ac9505",
      "type": "RELEASE",
      "amount_usd": "0.06",
      "available_after_usd": "8.53",
      "reserved_after_usd": "0.00",
      "workflow_run_id": "wrun_0123456789abcdef0123456789abcdef",
      "description": "Released unused reservation.",
      "created_at": "2026-07-14T08:31:20Z"
    }
  ],
  "next_cursor": null
}
```
