# Get a workflow

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

Returns one workflow, with its draft definition.

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

## Parameters

### Path

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

## Draft and live

`definition` is the draft, the version being edited. Production runs the published version, `latestVersionId`. `hasUnpublishedChanges` says the draft has moved on since.

The definition comes back with what GoRunner fills in on a save: a branch’s or loop’s `pieceId`, a branch’s `config.executionMode` and `config.branches`, and every `valid`. The stored draft isn’t changed, and saving it back as it came changes nothing.

`status` is `draft` (never published), `active` (live) or `paused` (was live, now off). `updatedAt` changes with edits only: publishing, pausing, tagging and filing leave it.

Keep `definitionRev`, and send it back as `expectedRev` when you [update the draft](https://docs.gorunner.io/api/workflows/update-workflow), so you never overwrite someone else’s save.

`webhookToken` is the secret part of the webhook address. Only a token with the **Run** permission for workflows gets it. A field that holds a secret, such as a password, always reads `[hidden secret: kept as set]`.

`sourceTemplate` names the template it was made from, while you can still see that template.

## Responses

- 200 OK

  The workflow.

- 400 `VALIDATION_ERROR`

  `id` isn’t a UUID.

- 403 `FORBIDDEN`

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

- 404 `NOT_FOUND`

  No workflow with this ID in the token’s workspace. A workflow in another workspace is a 404 too.

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 workflows
WORKFLOW_ID="your-workflow-id"

curl "$GORUNNER/api/workflows/$WORKFLOW_ID" \
  -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}`,
  {
    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}",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]
```

Response · 200 OK, shortened

```json
{
  "data": {
    "id": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
    "name": "Order intake",
    "definition": { … },
    "status": "active",
    "definitionRev": 7,
    "webhookToken": "2f4c8e1a-6b3d-4f7e-9a05-c1d2e3f4a5b6",
    "latestVersionId": "f20f671a-2c8e-4b9d-a7f1-6e3b0d5c9a12",
    "hasUnpublishedChanges": false,
    "updatedAt": "2026-10-24T16:40:00Z",
    "sourceTemplate": null
  }
}
```

**Every field**

It takes no body.

### Response · 200 OK

- `data` · object
  - `id` · UUID
  - `name` · string
  - `description` · string
  - `definition` · object: The workflow definition, and the single most important shape in this API. A workflow definition: its fields are in [Workflow definition](https://docs.gorunner.io/api/workflow-definition).
  - `status` · string: `draft` = never published. `active` = live, triggers registered and the webhook URL reachable. `paused` = was live, now switched off. Server-owned: it moves only through publish/unpublish, never through a create or update body. One of `draft`, `active`, `paused`.
  - `triggerType` · string: Mirrors `definition.trigger.type` for dispatch. Derived server-side from the definition by every update and publish, so treat it as read-only in practice. A create that sends its own value keeps it until then, unchecked. One of `manual`, `cron`, `webhook`, `callable`, `event`, `error`, `run_finished`, `poll`.
  - `cronExpression` · string or null: Nothing derives or reads this: the schedule runs from the trigger’s own `config`, and `nextRunAt` projects from it too. Null unless a caller wrote one with `PUT /api/workflows/{id}`.
  - `webhookToken` · string or null: The secret segment of the public webhook URL. Made by the first update of a webhook workflow, or at publish; absent until then (so absent in the answers of create, duplicate and import). Returned ONLY to a caller holding `workflow:run`: by `GET /api/workflows/{id}` and in the answers of update, publish, unpublish and restore (rotate answers it to its caller). It is never present on a list row, an export or a version snapshot. It is a bearer capability against an unauthenticated endpoint — possessing it is the ability to fire this workflow in production. Rotate it with `POST /api/workflows/{id}/webhook/rotate`.
  - `latestVersionId` · UUID or null: The published snapshot production executes. Absent means nothing has ever been published.
  - `definitionRev` · integer: Optimistic-lock revision, bumped by every definition save. Populated by `GET /api/workflows/{id}`, by the update response and by the answers of create, duplicate and import; absent (0) on list rows. Send it back as `expectedRev` on an update to refuse a clobbering write.
  - `hasUnpublishedChanges` · boolean: The draft has moved ahead of the published version. Computed on the single-workflow read and on every list row; always `false` for a workflow never published.
  - `folderId` · UUID or null: Absent/null = uncategorized. Changed only through `PUT /api/workflows/{id}/folder`.
  - `tags` · array of strings: Changed only through `PUT /api/workflows/{id}/tags` or the bulk `addTags` operation — never through the workflow update body.
  - `testPayloads` · object or null: The saved test-fixture library, stored outside the definition so publish and version restore never reset it.
  - `pinnedData` · object or null: The pin library (`{"pins": {"<stepName>": {"data": …, "pinnedAt": …, "configHash": …, "staleAt": …, "pieceId": …}}}`). A pinned step emits its fixture instead of executing, and only on test runs. `configHash`, `staleAt` and `pieceId` are the server’s: a step whose config changed keeps its pin marked stale, and a stale pin is not replayed (the step runs); a step whose app was replaced loses it.
  - `workspaceId` · UUID: Stamped from the credential’s scope; never accepted from a request body.
  - `orgId` · UUID
  - `createdBy` · UUID or null
  - `createdByName` · string: List rows only: the creator’s name, else their email address. Absent when the creator is unknown or not a member of the organization (staff who built it on a support visit, someone since removed), and on every row for an embedded person whose installation switched `presence` off (an embedded person is never told an address).
  - `createdAt` · time
  - `updatedAt` · time: When the workflow itself was last edited. Publishing, pausing, tagging and filing it are not edits and leave it alone.
  - `failing` · boolean: List rows only: the workflow is live (`status: active`) and its latest FINISHED production run (one that worked or failed) failed. A later run that is waiting, running, held or was cancelled changes nothing, and test runs never count. A paused or draft workflow is never failing.
  - `failingSince` · time: Only on a failing row: when the first production run failed after the last one that worked (runs cancelled, held or running in between neither break nor begin the streak). Left out for a caller without `run:read`.
  - `failingRun` · object: Only on a failing row: the run that decided it, its latest finished production run, with why it failed. It is `lastRun` unless a later run is waiting, running, held or was cancelled. Without `run:read` it carries `status` and `startedAt` alone.
    - `id` · UUID
    - `status` · string: One of `failed`.
    - `startedAt` · time
    - `finishedAt` · time or null
    - `error` · object: Why it failed, as `lastRun.error` says it.
      - `step` · string or null
      - `stepKey` · string or null
      - `message` · string
  - `summary` · object: List rows only: what a list draws instead of the definition.
    - `trigger` · object or null: The chosen trigger; null while none is chosen.
      - `pieceId` · string
      - `actionId` · string
      - `displayName` · string
    - `pieces` · array of objects: The apps the workflow uses, each once, in walk order (the trigger first), with their display names.
      - `id` · string
      - `name` · string
    - `stepCount` · integer: Steps after the trigger, across branch arms, loop bodies and error paths.
    - `needsSetup` · integer: Steps (and a chosen trigger) the editor marks to configure: `valid: false`, or no app and action chosen.
  - `runs7d` · array of integers: List rows without `view=summary` only: counted runs per UTC day over the last seven days, oldest first (production runs that are not a sub-flow’s, cancelled runs left out).
  - `nextRunAt` · time: Next fire time of an active schedule workflow, projected from its cron in the trigger’s own timezone. Populated by the list endpoint.
  - `lastRun` · object or null: The most recent PRODUCTION run’s outcome (test runs never count). Populated on list rows only. Without `run:read` it carries `status` and `startedAt` alone.
    - `id` · UUID
    - `finishedAt` · time or null
    - `error` · object: Only when `status` is `failed`: why.
      - `step` · string or null: The display name of the step it failed at; null when the reason names no step.
      - `stepKey` · string or null
      - `message` · string: The reason’s first clause, at most 160 characters.
    - `status` · string: One of `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`.
    - `startedAt` · time
  - `sourceTemplate` · object or null: The template this workflow was made from, while it exists and the caller may see it (the gallery’s own rule: this organization’s, this customer workspace’s, or an installation template still offered); null otherwise. Only this read carries it.
    - `id` · UUID
    - `name` · string
  - `triggerSubscription` · object: The trigger’s registration with its provider, for a vendor webhook trigger that registers itself on publish (absent otherwise): `registering`, `active`, or `failed` with the provider’s refusal in `lastError` and the next try in `nextAttemptAt`. A subscription that expires (`expiresAt`) is renewed by the workers before it does. Only this read carries it.
    - `state` · string: One of `registering`, `active`, `failed`.
    - `piece` · string
    - `lastError` · string
    - `lastErrorAt` · time
    - `nextAttemptAt` · time
    - `expiresAt` · time
    - `registeredAt` · time
    - `details` · object: What the registration listens to, by the provider’s own ids — only safe facts, per app: GitHub `owner`/`repository`, GitLab `project`, Asana `resource`/`workspace`, ClickUp `team`, Jira `site`, Linear `teamId`, Trello `idModel`, Mailchimp `list`, Pipedrive `object`/`action`, Shopify `topic`, Calendly `scope`/`organization`/`user`, Typeform `formId`/`tag`, Tally and Jotform `formId`, Twilio `phoneNumber`, WhatsApp `phoneNumberId`/`businessAccountId`. Absent when there is none. Never a secret or an address that carries one.
      - `{key}` · string: Each key’s value.
    - `requestUrl` · string: For a trigger whose app sends every event to one address (Slack with your own Slack app): the Request URL to paste into the app’s Event Subscriptions once. Not a secret: every delivery is verified with the app’s Signing Secret.

Related: [Update a workflow’s draft](https://docs.gorunner.io/api/workflows/update-workflow) · [List versions](https://docs.gorunner.io/api/publish/list-workflow-versions) · [Call a workflow’s webhook](https://docs.gorunner.io/api/webhooks)

Source: https://docs.gorunner.io/api/workflows/get-workflow
