# List a workflow’s runs

**GET** `/api/workflows/{id}/runs`

Returns one workflow’s runs, newest first: production and test together.

Needs [an API token](https://docs.gorunner.io/api/authentication#scopes) with the **Read** permission for runs (`run:read`).

## Parameters

### Path

- `id` · Required · UUID: The workflow’s ID.

### Query

- `page` · Optional · integer: The page, from 1.
- `limit` · Optional · integer: Runs a page, 1 to 100. The default is 20; anything else reads as 20, even 500.

## Read it

There is no environment filter: tell runs apart by `environment`. Each row has its `triggerData`. A run waiting in line reads `pending` here.

`versionId` is the live version when the run started. For what actually ran (`executedVersionId`) and the steps, use [Get a run](https://docs.gorunner.io/api/runs/get-run).

## Responses

- 200 OK

  A page of runs. None answers `[]`.

- 400 `VALIDATION_ERROR`

  `id` isn’t a UUID.

- 403 `FORBIDDEN`

  The token lacks the **Read** permission for runs.

- 404 `NOT_FOUND`

  No workflow with this ID in the token’s workspace.

- 500 `INTERNAL_ERROR`

  The list couldn’t be read. Try again.

Any call can also answer: [`401 UNAUTHORIZED`](https://docs.gorunner.io/errors#UNAUTHORIZED) · [`401 TOKEN_ENDED`](https://docs.gorunner.io/errors#TOKEN_ENDED) · [`403 TOKEN_SURFACE_OFF`](https://docs.gorunner.io/errors#TOKEN_SURFACE_OFF) · [`403 FEATURE_OFF`](https://docs.gorunner.io/errors#FEATURE_OFF) · [`403 EMBED_NOT_ALLOWED`](https://docs.gorunner.io/errors#EMBED_NOT_ALLOWED) · [`403 EMBED_NOT_AVAILABLE`](https://docs.gorunner.io/errors#EMBED_NOT_AVAILABLE) · [`403 ACCOUNT_SUSPENDED`](https://docs.gorunner.io/errors#ACCOUNT_SUSPENDED) · [`429 RATE_LIMITED`](https://docs.gorunner.io/errors#RATE_LIMITED) · [`503 BUSY`](https://docs.gorunner.io/errors#BUSY) · [`503 PAUSED`](https://docs.gorunner.io/errors#PAUSED) · [`503 SERVICE_UNAVAILABLE`](https://docs.gorunner.io/errors#SERVICE_UNAVAILABLE). See [Errors](https://docs.gorunner.io/api/errors#by-status).

Request

**curl**

```bash
GORUNNER="https://api.gorunner.io"
GORUNNER_TOKEN="grt_…"  # needs Read for runs
WORKFLOW_ID="your-workflow-id"

curl "$GORUNNER/api/workflows/$WORKFLOW_ID/runs" \
  -H "Authorization: Bearer $GORUNNER_TOKEN"
```

**JavaScript**

```js
const GORUNNER = "https://api.gorunner.io";
const TOKEN = process.env.GORUNNER_TOKEN;
const WORKFLOW_ID = process.env.WORKFLOW_ID;

const res = await fetch(
  `${GORUNNER}/api/workflows/${WORKFLOW_ID}/` +
    `runs`,
  {
    headers: { Authorization: `Bearer ${TOKEN}` },
  },
);
const { data } = await res.json();
```

**Python**

```python
import os, requests

GORUNNER = "https://api.gorunner.io"
TOKEN = os.environ["GORUNNER_TOKEN"]
WORKFLOW_ID = os.environ["WORKFLOW_ID"]

res = requests.get(
    f"{GORUNNER}/api/workflows/{WORKFLOW_ID}/runs",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]
```

Response · 200 OK, shortened

```json
{
  "data": [
    {
      "id": "8f0c41d2-6b7e-4c1a-9d3f-2a5b7c9e1f04",
      "workflowId": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
      "status": "completed",
      "triggerType": "api",
      "startedAt": "2026-10-25T09:00:00Z",
      "finishedAt": "2026-10-25T09:00:01Z",
      "durationMs": 640,
      "environment": "production",
      "versionId": "f20f671a-2c8e-4b9d-a7f1-6e3b0d5c9a12",
      "versionNumber": 3
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1
  }
}
```

**Every field**

It takes no body.

### Response · 200 OK

- `data` · array of objects: The page’s runs; `[]` when it has none.
  - `id` · UUID: The run id. This is what a 202 gives you to poll or stream.
  - `workflowId` · UUID
  - `workspaceId` · UUID
  - `orgId` · UUID
  - `status` · string: `pending` = queued, nothing has executed. `waiting` (with `?wait=`) = accepted and still waiting in line for a free slot after the wait: a runs-at-once limit holds it, and it starts in order. `paused` = suspended on a durable wait (delay/approval) and will continue — it is in flight, not finished. One of `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`, `waiting`.
  - `triggerType` · string: How the run started. `api` = a production run through `POST /api/workflows/{id}/run`; `mcp` = the same door, by an AI app’s tool. `manual` = a TEST run through that same endpoint (kept as `manual` deliberately — the builder’s step-data picker matches it literally). Also `webhook` (a hosted form’s submission too), `cron`, `poll`, `error`, `run_finished`, `subflow`.
  - `triggerData`: The trigger payload this run received, in delivery shape (for a webhook: `method`/`headers`/`query`/`body`). This is exactly what `{{trigger.…}}` resolves against, and what you would replay as a `__shaped` body.
  - `startedAt` · time: When the run was accepted. It may then wait in line for a free slot before a worker starts it (`firstStartedAt`).
  - `firstStartedAt` · time or null: When a worker first started the run. `durationMs` counts from here, so the time from `startedAt` to this is the time the run waited in line. Null until a worker starts the run, and for a run started on v2026.10.5 or earlier or one that ran inline (a Sub-flow’s, an error handler’s), whose `durationMs` counts from `startedAt`.
  - `finishedAt` · time or null: Absent while the run is in flight.
  - `durationMs` · integer or null: How long the run took, in milliseconds: from the first time a worker started the run (`firstStartedAt`, else `startedAt`) to its finish. Absent while the run is in flight.
  - `errorMessage` · string
  - `tags` · array of strings or null: `null` on the 202 of a run just accepted, `[]` when a finished run has none.
  - `environment` · string: Deliberately independent of `triggerType` — a listen-captured delivery is honestly a webhook run AND honestly a test. `GET /api/runs` defaults to production only; this workflow-scoped list returns both. One of `test`, `production`.
  - `versionId` · UUID or null: The latest published version at the moment the run started — NOT necessarily what executed.
  - `versionNumber` · integer or null
  - `executedVersionId` · UUID or null: What ACTUALLY executed. Neither answer of this shape carries it, whatever ran (the 202 without `?wait=`, `GET /api/workflows/{id}/runs`): read `GET /api/runs/{id}`, where absent means the DRAFT ran, so rendering `versionId`'s snapshot would show a graph that never ran.
  - `executedVersionNumber` · integer or null: Display number of `executedVersionId`; carried only where it is.
  - `startedFromStep` · string or null: The step a PARTIAL (`fromNode`) run began at. Empty/absent means the run started at its trigger, which is every ordinary run — without it the trigger would be marked complete on a run that never executed it.
  - `retryOfRunId` · UUID or null: Lineage only, set from `?retryOf=`.
  - `steps` · array of objects: Per-step results. NOT populated by the run list or by the 202 from the run endpoint — fetch `GET /api/runs/{id}`. Each entry’s `outputData` is that step’s output, keyed to the step by `nodeId`.
- `pagination` · object: Paging envelope returned alongside `data` on list endpoints.
  - `page` · integer: The 1-based page that was returned.
  - `limit` · integer: Rows per page actually applied (not necessarily what you asked for — out-of-range values fall back to 20).
  - `total` · integer: Total matching rows across all pages.
  - `totalPages` · integer

Related: [List runs](https://docs.gorunner.io/api/runs/list-runs) · [Get a run](https://docs.gorunner.io/api/runs/get-run)

Source: https://docs.gorunner.io/api/runs/list-workflow-runs
