# Create a workflow

**POST** `/api/workflows`

Makes a workflow. It always starts as a draft.

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

## Parameters

### Headers

- `Idempotency-Key` · Optional · string: Up to 255 characters. The same request with the same key within 24 hours answers the first result again instead of making a second workflow. See Idempotency.

### Body

- `name` · Optional · string: Up to 255 characters. Left out or empty: “Untitled workflow”.
- `description` · Optional · string: A line about what it does.
- `definition` · Optional · object: The whole tree: the trigger, then each step through `nextAction`. See [Workflow definition](https://docs.gorunner.io/api/workflow-definition).
- `tags` · Optional · array of strings: Up to 50 tags of up to 50 characters each. Trimmed, blanks dropped, repeats removed ignoring case; the first spelling stays.
- `folderId` · Optional · UUID or null: A folder in this workspace to file it in.

**What are the rarely used parameters?**

- `triggerType` · Optional · string: Leave it out. GoRunner reads it from the trigger.

## What GoRunner fills in

Leave out `definition` to get an empty workflow with a manual trigger, ready for [Update a workflow’s draft](https://docs.gorunner.io/api/workflows/update-workflow).

Send one, and GoRunner checks its shape, removes any webhook token in it, and pins each step without a `pieceVersion` to the app version it runs today. A branch or loop without a `pieceId` gets its app, a branch without `config.executionMode` gets `FIRST_MATCH`, a branch’s arms with conditions are copied into an empty `config.branches`, a branch without an `Otherwise` arm gets one, a step without a `displayName` gets its action’s, and `valid` is worked out. A definition without `schemaVersion` gets 2. See [Workflow definition](https://docs.gorunner.io/api/workflow-definition).

The answer has no `webhookToken`: a webhook workflow gets its address when you first update it or publish it. A `triggerType` you send is kept as sent until then.

It doesn’t check that the apps exist. [Check a definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) first does. To put it live, [publish it](https://docs.gorunner.io/api/publish/publish-workflow).

## Responses

- 201 Created

  The new draft, with its `id` and `definitionRev`.

- 400 `VALIDATION_ERROR`

  The body isn’t JSON, the name is over 255 characters, there are over 50 tags or a tag is over 50 characters, the tree has a key it doesn’t take (the message names it), a step’s name is missing or used twice, a step’s name or display name is over 255 characters, a step’s `type` is unknown, a branch has no arm, a condition has an operator that doesn’t exist (the message lists the ones that do), or the folder isn’t in this workspace.

- 403 `FORBIDDEN`

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

- 403 `FEATURE_OFF`

  A token made inside a partner’s product sent `folderId` while folders are switched off there.

- 409 `CONFLICT`

  A request with this `Idempotency-Key` is still in progress, or its answer was too large to keep. Try again shortly.

- 422 `VALIDATION_ERROR`

  This `Idempotency-Key` was already used with a different request.

- 422 `PIECE_NOT_ALLOWED`

  A step uses an app this workspace may not use. The message names the first one.

- 500 `INTERNAL_ERROR`

  The workflow wasn’t made. 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 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

curl -X POST "$GORUNNER/api/workflows" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order intake",
    "tags": [
      "orders"
    ],
    "definition": {
      "schemaVersion": 2,
      "trigger": {
        "name": "trigger",
        "type": "webhook",
        "pieceId": "webhook",
        "actionId": "catch_webhook",
        "config": {
          "methods": [
            "POST"
          ]
        },
        "valid": true
      },
      "settings": {}
    }
  }'
```

**JavaScript**

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

const res = await fetch(`${GORUNNER}/api/workflows`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Order intake",
    tags: ["orders"],
    definition: {
      schemaVersion: 2,
      trigger: {
        name: "trigger",
        type: "webhook",
        pieceId: "webhook",
        actionId: "catch_webhook",
        config: {
          methods: ["POST"],
        },
        valid: true,
      },
      settings: {},
    },
  }),
});
const { data } = await res.json();
```

**Python**

```python
import os, uuid, requests

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

res = requests.post(
    f"{GORUNNER}/api/workflows",
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
      "name": "Order intake",
      "tags": ["orders"],
      "definition": {
        "schemaVersion": 2,
        "trigger": {
          "name": "trigger",
          "type": "webhook",
          "pieceId": "webhook",
          "actionId": "catch_webhook",
          "config": {
            "methods": ["POST"],
          },
          "valid": True,
        },
        "settings": {},
      },
    },
)
data = res.json()["data"]
```

Response · 201 Created, shortened

```json
{
  "data": {
    "id": "6e9a1c3f-8b2d-4a57-9e0c-3f7b5d1a8c42",
    "name": "Order intake",
    "definition": {
      "schemaVersion": 2,
      "trigger": {
        "name": "trigger",
        "type": "webhook",
        "pieceId": "webhook",
        "pieceVersion": "1.0.0",
        "actionId": "catch_webhook",
        "config": { … }
      }
    },
    "status": "draft",
    "definitionRev": 1,
    "triggerType": "webhook",
    "hasUnpublishedChanges": false
  }
}
```

**Every field**

### Body

- `name` · Optional · string: Trimmed. Defaults to "Untitled workflow" when empty, spaces only or omitted.
- `description` · Optional · string
- `definition` · Optional · object: Omit for a blank draft. A workflow definition: its fields are in [Workflow definition](https://docs.gorunner.io/api/workflow-definition).
- `triggerType` · Optional · string: Almost always leave this out: it is derived from `definition.trigger` (falling back to `manual`). A value sent is kept as sent, unchecked, until the next `PUT` or publish derives it from the definition. One of `manual`, `cron`, `webhook`, `callable`, `event`, `error`, `run_finished`, `poll`.
- `folderId` · Optional · UUID or null: Folder to file the workflow under. Must be a folder in the same workspace or the create is refused with 400.
- `tags` · Optional · array of strings: Each at most 50 characters. Trimmed, blank entries dropped, deduplicated case-insensitively (the first spelling wins). More than 50 distinct tags, or one over 50 characters, is a 400.

### Response · 201 Created

- `data` · object: A workflow row. `definition` is always the DRAFT — the tree the editor holds. What production actually runs is the snapshot named by `latestVersionId`, which only changes when you publish.
  - `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

Related: [Check a definition](https://docs.gorunner.io/api/workflows/validate-workflow-definition) · [Publish a workflow](https://docs.gorunner.io/api/publish/publish-workflow) · [Workflow definition](https://docs.gorunner.io/api/workflow-definition) · [Idempotency](https://docs.gorunner.io/api/idempotency)

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