> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://onepin.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://onepin.ai/docs/_mcp/server.

# Run a workflow

A run executes a workflow's pipeline and produces audio. Start a run, poll until it's done, then download the result.

## Start and watch a run

#### Python

```python
import time
from onepin import OnepinClient

client = OnepinClient()

# `paused` is not terminal, but it is not progressing either — stop waiting on it.
SETTLED = ("completed", "failed", "cancelled", "paused")

run = client.workflows.runs.start(workflow_id).data
while run.status not in SETTLED:
    time.sleep(3)
    run = client.workflows.runs.status(workflow_id, run.id).data
    print(run.status, f"{run.finished_steps or 0}/{run.total_steps or 0}")

if run.status != "completed":
    raise RuntimeError(f"run {run.status}: {run.error}")
```

#### CLI

```bash
# --watch polls every 2s until the run reaches a terminal state
onepin workflows run "$WORKFLOW_ID" --watch
```

`Ctrl+C` stops watching — the run keeps executing on the server. Cancel it with `onepin workflows runs cancel "$WORKFLOW_ID" "$RUN_ID"`.

> **Warning**
>
> `--watch` treats only `completed`, `failed`, and `cancelled` as an end state, so it keeps polling a **paused** run until `--timeout` expires. If a watch seems stuck, check the run's status directly.

There are no completion webhooks yet — polling is the way. Short scripts finish in seconds.

## Runs can pause

A run isn't only heading for `completed`, `failed`, or `cancelled`. It can also park in **`paused`** — and not just because someone asked it to. Onepin pauses a run automatically rather than burning retries against a problem that won't resolve on its own:

| `pause_reason`          | What happened                               |
| ----------------------- | ------------------------------------------- |
| `user`                  | Someone called pause                        |
| `provider_outage`       | The TTS provider is down                    |
| `provider_rate_limited` | The provider is throttling                  |
| `provider_billing`      | A provider billing problem blocked the call |
| `node_timeout`          | A node ran past its deadline                |

A paused run keeps everything it has already produced — the current wave of parallel work is allowed to finish before it parks, so nothing in flight is thrown away. `pause_error` carries the customer-facing explanation, and `held_nodes` lists the nodes it is parked in front of.

Resume it when the cause has cleared, or stop it for good:

#### Python

```python
client.workflows.resume_run(workflow_id, run.id)   # picks up from the last completed wave
client.workflows.runs.cancel(workflow_id, run.id)  # give up on it instead
```

Resuming re-runs nothing that already completed. It returns `409` if the workspace already has another active run for this workflow or you're at the concurrent-run limit — the run stays `paused` and you can retry.

#### API

```bash
curl -X POST "https://api.onepin.ai/api/v1/workflows/$WF/runs/$RUN/resume" \
  -H "Authorization: Bearer $ONEPIN_API_KEY"
```

Pausing and resuming need at least the `editor` role in the workspace.

## Download the result

A completed run's export is a ZIP of the audio files plus a `manifest.csv`. The download URL is pre-signed and valid for 15 minutes.

#### Python

```python
import httpx

dl = client.workflows.download_run(workflow_id, run.id).data
resp = httpx.get(dl.url)
resp.raise_for_status()
with open(dl.filename, "wb") as f:
    f.write(resp.content)
```

#### CLI

```bash
onepin workflows runs download "$WORKFLOW_ID" "$RUN_ID" --out audio.zip
```

Only `completed` runs are downloadable — active, `failed`, or `cancelled` runs return `409`, and a run with no audio returns `404`.

## List workflows and run history

#### Python

```python
for wf in client.workflows.list(limit=50).data:
    print(wf.id, wf.name)

for r in client.workflows.runs.list(workflow_id, limit=20).data:
    print(r.id, r.status, r.created_at)
```

#### CLI

```bash
onepin workflows list
onepin workflows runs list "$WORKFLOW_ID"
```

## Related

* [Use your own script](/docs/guides/use-your-own-script) — run with per-request text
* [Use templates](/docs/guides/use-templates) — create a workflow from a template
* [Workflow](/docs/workflow/overview) — what's inside a workflow
* [Handle errors](/docs/guides/handle-errors) — typed exceptions and retries