# Publish a workflow

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

Makes the draft the live version and turns the workflow on.

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 are the rarely used parameters?**

- `review` · Optional · object: Only for Keel’s publish card. Leave it out.

## What it does

The draft is saved as a new version, and `status` becomes `active`. Live triggers run that version. Editing the draft afterwards changes nothing live until you publish again.

A production run through [Run a workflow](https://docs.gorunner.io/api/runs/run-workflow) needs a published workflow.

Publishing an unchanged draft makes no new version and still answers 200, so it is safe to repeat. It takes no `Idempotency-Key`. It doesn’t change `updatedAt`. `definitionRev` moves every time, so take the new one from the answer before sending `expectedRev` again.

## The webhook address

The first publish of a webhook workflow makes its `webhookToken`, which the answer holds if the token you call with has the **Run** permission for workflows. Its address, `/api/webhooks/{webhookToken}`, takes runs without a token: whoever has it can start production runs.

If it leaks, [get a new webhook address](https://docs.gorunner.io/api/publish/rotate-workflow-webhook-token). Publishing again also signs a self-registering trigger, such as Stripe’s, up with its service again. If that fails, the publish still goes through.

## What it refuses

Any problem that [Check a definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) marks blocking, and also: a schedule that can’t be read, an approval link nothing waits for, an error workflow that can’t run, or an error or finished trigger watching a workflow it can’t watch.

A step whose connection is missing from the workspace answers `details.code` `connection_not_found`. One made for another service answers `connection_wrong_piece`. A trigger may use its action app’s connection, such as a Gmail trigger with a Gmail connection.

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/publish"
curl -X POST "$URL" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedRev": 7
  }'
```

**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}/` +
    `publish`,
  {
    method: "POST",
    headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
    body: JSON.stringify({ expectedRev: 7 }),
  },
);
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"publish",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={"expectedRev": 7},
)
data = res.json()["data"]
```

Response · 200 OK, shortened

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

## Responses

- 200 OK

  Live. `latestVersionId` names the version production now runs.

- 400 `VALIDATION_ERROR`

  `id` isn’t a UUID, or the body isn’t JSON, 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.

- 409 `KEEL_PUBLISH_STALE`

  Only for Keel’s publish card: the draft or the last version moved since the card was shown. Nothing was published.

- 422 `VALIDATION_ERROR`

  The draft can’t go live. The message names the step; `details` has `code`, `step`, `stepKey`, `field` and `alternatives`.

- 422 `PIECE_NOT_ALLOWED`

  A step uses an app this workspace may not use. It is named before any other problem.

- 500 `INTERNAL_ERROR`

  Nothing went live. Try again.

- 503 `PIECES_UNAVAILABLE`

  GoRunner couldn’t check which apps this workspace may use. Nothing changed. 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).

Response · 422 Unprocessable Entity

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "“Forward to fulfilment” names a connection this workspace doesn’t have (0f8b2c7e-…). Choose one of its HTTP Request connections, or connect HTTP Request under Connections first.",
    "details": {
      "code": "connection_not_found",
      "step": "Forward to fulfilment",
      "stepKey": "forward",
      "field": "connectionId"
    }
  }
}
```

**Every field**

### Body

- `expectedRev` · Optional · integer: The `definitionRev` of the draft the caller holds. Makes the publish conditional: a draft that moved since gives 409 and nothing is published. Omit — or send 0 or less, which is ignored — for an unconditional publish.
- `review` · Optional · object: Required on a person’s click on the assistant’s publish card (`X-Assistant-Chat` with `X-Assistant-Click`), ignored otherwise: the comparison the card showed. 409 `KEEL_PUBLISH_STALE` when it no longer describes the workflow.
  - `rev` · Optional · integer: The `definitionRev` of the draft the card showed.
  - `baseVersionId` · Optional · string: The last version a person published, which the card compared the draft with; empty for a first publish.

### 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: [Unpublish a workflow](https://docs.gorunner.io/api/publish/unpublish-workflow) · [Review what publishing changes](https://docs.gorunner.io/api/publish/get-publish-review) · [Run a workflow](https://docs.gorunner.io/api/runs/run-workflow) · [Call a workflow’s webhook](https://docs.gorunner.io/api/webhooks)

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