# Unpublish a workflow

**POST** `/api/workflows/{id}/unpublish`

Takes a workflow offline: its triggers stop, and its draft and versions stay.

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

## Parameters

### Path

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

### Body

Optional.

- `expectedRev` · Optional · integer: The `definitionRev` you read. If the draft changed since, nothing happens and you get 409. Left out, 0 or less: no check.

## What stops, and what stays

Schedules skip it, and its webhook address answers 404. Runs already going are not cancelled.

The webhook address stays the same, so publishing again brings it back. A self-registering trigger is signed off from its service. A trigger that checks a service for new items starts from now when you publish again, not from where it stopped.

Its runs waiting in line stay. [Cancel them](https://docs.gorunner.io/api/runs/cancel-waiting-runs) if you don’t want them.

## Responses

- 200 OK

  Paused. A paused workflow answers 200 as it is, and a draft stays a draft.

- 400 `VALIDATION_ERROR`

  `id` isn’t a UUID, or `expectedRev` isn’t a whole number.

- 403 `FORBIDDEN`

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

- 403 `KEEL_CLICK_REQUIRED`

  Only Keel’s own publish card sends this: a write no person clicked. An API call never gets it.

- 404 `NOT_FOUND`

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

- 409 `CONFLICT`

  `expectedRev` is out of date: someone saved since. Nothing changed. Read the workflow again.

- 409 `CONFLICT`

  Another write to this workflow took too long. Try again.

- 500 `INTERNAL_ERROR`

  The workflow is still live. 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) · [`413 PAYLOAD_TOO_LARGE`](https://docs.gorunner.io/errors#PAYLOAD_TOO_LARGE) · [`415 INVALID_CONTENT_TYPE`](https://docs.gorunner.io/errors#INVALID_CONTENT_TYPE) · [`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 Publish for workflows
WORKFLOW_ID="your-workflow-id"

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

Response · 200 OK, shortened

```json
{
  "data": {
    "id": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
    "name": "Order intake",
    "status": "paused",
    "triggerType": "webhook",
    "latestVersionId": "f20f671a-2c8e-4b9d-a7f1-6e3b0d5c9a12",
    "definitionRev": 7,
    "updatedAt": "2026-10-24T16:40:00Z"
  }
}
```

**Every field**

### Body

- `expectedRev` · Optional · integer: The `definitionRev` of the draft the caller holds. Makes the write conditional: a draft that moved since gives 409 and nothing is written. Omit — or send 0 or less, which is ignored — for an unconditional write.

### Response · 200 OK

- `data` · object: A workflow. `definition` is the DRAFT — what the editor saves into. Production executes the snapshot named by `latestVersionId` instead, which is why `hasUnpublishedChanges` matters.
  - `id` · UUID
  - `name` · string
  - `description` · string
  - `definition` · object: The workflow definition. It is a TREE, not a list of steps: the trigger holds `nextAction`, each action holds the next one, and branches/loops/error paths nest their own sub-chains. A flat `actions` array is not a definition and would execute nothing. 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, public webhook reachable. `paused` = was live, toggled off. One of `draft`, `active`, `paused`.
  - `triggerType` · string: Derived from the definition’s trigger on every write. Only `webhook` workflows have a `webhookToken`. One of `manual`, `cron`, `webhook`, `error`, `run_finished`, `poll`.
  - `cronExpression` · string or null
  - `webhookToken` · string or null: The workflow’s public production door. `POST /api/webhooks/{webhookToken}` is UNAUTHENTICATED — the unguessable URL is the credential. Minted by publish for a webhook workflow, revoked and replaced by `POST /api/workflows/{id}/webhook/rotate`. Present here only for a credential holding `workflow:run`; redacted below that, and never present in the workflow LIST.
  - `latestVersionId` · UUID or null: The published snapshot production runs. Null until first publish.
  - `definitionRev` · integer: Optimistic-lock revision, bumped on every save. Pass it back as `expectedRev` to make a write conditional. Present on the single-workflow read and on write responses; absent (0) in lists.
  - `hasUnpublishedChanges` · boolean: True when the draft has moved ahead of what is live. Computed in SQL, so it is exact — undoing an edit back to the published bytes reads false again.
  - `folderId` · UUID or null
  - `tags` · array of strings
  - `testPayloads` · object or null: Saved test fixtures. Stored outside the definition, so publish/restore never resets them.
  - `pinnedData` · object or null: Frozen step outputs replayed instead of executing, TEST runs only. Stored outside the definition.
  - `workspaceId` · UUID
  - `orgId` · UUID
  - `createdBy` · UUID or null
  - `createdAt` · time
  - `updatedAt` · time

Related: [Publish a workflow](https://docs.gorunner.io/api/publish/publish-workflow) · [Cancel waiting runs](https://docs.gorunner.io/api/runs/cancel-waiting-runs)

Source: https://docs.gorunner.io/api/publish/unpublish-workflow
