# Go back to a version

**POST** `/api/workflows/{id}/versions/{versionId}/restore`

Copies a published version into the draft. What runs live doesn’t change until you publish.

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

## Parameters

### Path

- `id` · Required · UUID: The workflow’s ID.
- `versionId` · Required · UUID: The version to bring back.

### 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 changes

Only the draft. Restore is checked like a save, so an old version that can’t run is refused here, not at the next publish. Then [publish](https://docs.gorunner.io/api/publish/publish-workflow) to put it live.

Test data and pins are kept apart from the tree, so they survive. Pins whose steps changed are marked stale. `triggerType` follows the restored tree, and a webhook workflow with no token gets one.

## Responses

- 200 OK

  The workflow with the version as its draft. `hasUnpublishedChanges` says whether that differs from what is live.

- 400 `VALIDATION_ERROR`

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

- 403 `FORBIDDEN`

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

- 404 `NOT_FOUND`

  No such workflow in the workspace, or the version isn’t one of its own.

- 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.

- 422 `VALIDATION_ERROR`

  The version fails the checks a save makes, such as an app this installation can no longer run on a live workflow.

- 422 `PIECE_NOT_ALLOWED`

  The version has a step on an app this workspace may not use, and the draft doesn’t.

- 500 `INTERNAL_ERROR`

  Nothing was written. 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).

Request

**curl**

```bash
GORUNNER="https://api.gorunner.io"
GORUNNER_TOKEN="grt_…"  # needs Write for workflows
WORKFLOW_ID="your-workflow-id"
VERSION_ID="your-version-id"  # from List versions

URL="$GORUNNER/api/workflows/$WORKFLOW_ID/versions/"
URL="${URL}$VERSION_ID/restore"
curl -X POST "$URL" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedRev": 8
  }'
```

**JavaScript**

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

const res = await fetch(
  `${GORUNNER}/api/workflows/${WORKFLOW_ID}/` +
    `versions/${VERSION_ID}/restore`,
  {
    method: "POST",
    headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
    body: JSON.stringify({ expectedRev: 8 }),
  },
);
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"]
VERSION_ID = os.environ["VERSION_ID"]  # from List versions

res = requests.post(
    f"{GORUNNER}/api/workflows/{WORKFLOW_ID}/"
    f"versions/{VERSION_ID}/restore",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={"expectedRev": 8},
)
data = res.json()["data"]
```

Response · 200 OK, shortened

```json
{
  "data": {
    "id": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
    "name": "Order intake",
    "definition": {
      "schemaVersion": 2,
      "trigger": {
        "name": "trigger",
        "displayName": "Order received",
        "type": "webhook",
        "pieceId": "webhook",
        "pieceVersion": "1.0.0",
        "actionId": "catch_webhook",
        "config": {
          "methods": [
            "POST"
          ]
        },
        "valid": true,
        "nextAction": {
          "name": "forward",
          "displayName": "Forward to fulfilment",
          "type": "PIECE",
          "pieceId": "http",
          "pieceVersion": "1.0.0",
          "actionId": "send_request",
          "config": {
            "method": "POST",
            "url": "https://example.com/fulfilment",
            "body_type": "json",
            "body_json": "{{trigger.body}}"
          },
          "valid": true
        }
      },
      "settings": {}
    },
    "status": "active",
    "latestVersionId": "f20f671a-2c8e-4b9d-a7f1-6e3b0d5c9a12",
    "definitionRev": 9,
    "hasUnpublishedChanges": true
  }
}
```

**Every field**

### Body

- `expectedRev` · Optional · integer: The workflow’s current `definitionRev`. Makes the restore conditional: a draft that moved underneath you gives 409 and writes nothing. Omit — or send 0 or less, which is ignored — for an unconditional restore.

### 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: [List versions](https://docs.gorunner.io/api/publish/list-workflow-versions) · [Publish a workflow](https://docs.gorunner.io/api/publish/publish-workflow) · [Test, publish and go back to a version](https://docs.gorunner.io/build/test-and-publish)

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