# Check a definition

**POST** `/api/workflows/validate`

Checks a definition and lists every problem, without saving anything.

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

## Parameters

### Body

Send at least one of the two.

- `definition` · Optional · object: A tree you are about to save.
- `workflowId` · Optional · UUID: A saved workflow, to check its draft. Ignored when `definition` is sent too: the tree is then checked as that workflow’s.

## Read the report

`ok` is `true` when nothing blocks a save or a publish. Warnings and notes can still be listed.

Each problem has a `severity`, a stable `code` to branch on, its `path` in the tree, the `step` and `field`, and `alternatives`: the values allowed there, nearest first.

A problem with `blocking: true` is exactly what a publish refuses. A step on an app the workspace may not use is `piece_not_allowed`. With `workflowId`, a Sub-flow step that calls that same workflow is `subflow_self`. A `workflowId` ignored beside a `definition` is a note, `workflow_id_ignored`. A **Return Response** header the run would refuse is a warning, `response_header_refused`.

The list stops at 200; `truncated` counts the rest. `skipped` names checks that couldn’t run, often because no worker answered. A short list with `skipped` isn’t a clean result.

## Responses

- 200 OK

  The report, whether or not the definition is valid. Read `ok`, not the status.

- 400 `VALIDATION_ERROR`

  The body isn’t JSON, it has neither `definition` nor `workflowId`, or `workflowId` alone isn’t a UUID.

- 403 `FORBIDDEN`

  The token lacks the **Read** permission for workflows. Checking changes nothing, so reading is enough.

- 404 `NOT_FOUND`

  No workflow with that `workflowId` in the workspace. Beside a `definition`, such an ID is ignored.

- 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 Read for workflows

curl -X POST "$GORUNNER/api/workflows/validate" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "definition": {
      "schemaVersion": 2,
      "trigger": {
        "name": "trigger",
        "type": "webhook",
        "pieceId": "webhook",
        "actionId": "catch_webhook",
        "config": {
          "methods": [
            "POST"
          ]
        },
        "valid": true,
        "nextAction": {
          "name": "forward",
          "type": "PIECE",
          "pieceId": "http",
          "pieceVersion": "1.0.0",
          "actionId": "sendRequest",
          "config": {
            "method": "POST",
            "url": "https://example.com/fulfilment"
          },
          "valid": true
        }
      },
      "settings": {}
    }
  }'
```

**JavaScript**

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

const res = await fetch(
  `${GORUNNER}/api/workflows/validate`,
  {
    method: "POST",
    headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
    body: JSON.stringify({
        definition: {
          schemaVersion: 2,
          trigger: {
            name: "trigger",
            type: "webhook",
            pieceId: "webhook",
            actionId: "catch_webhook",
            config: {
              methods: ["POST"],
            },
            valid: true,
            nextAction: {
              name: "forward",
              type: "PIECE",
              pieceId: "http",
              pieceVersion: "1.0.0",
              actionId: "sendRequest",
              config: {
                method: "POST",
                url: "https://example.com/fulfilment",
              },
              valid: true,
            },
          },
          settings: {},
        },
      }),
  },
);
const { data } = await res.json();
```

**Python**

```python
import os, requests

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

res = requests.post(
    f"{GORUNNER}/api/workflows/validate",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
      "definition": {
        "schemaVersion": 2,
        "trigger": {
          "name": "trigger",
          "type": "webhook",
          "pieceId": "webhook",
          "actionId": "catch_webhook",
          "config": {
            "methods": ["POST"],
          },
          "valid": True,
          "nextAction": {
            "name": "forward",
            "type": "PIECE",
            "pieceId": "http",
            "pieceVersion": "1.0.0",
            "actionId": "sendRequest",
            "config": {
              "method": "POST",
              "url": "https://example.com/fulfilment",
            },
            "valid": True,
          },
        },
        "settings": {},
      },
    },
)
data = res.json()["data"]
```

Response · 200 OK, with one problem

```json
{
  "data": {
    "ok": false,
    "diagnostics": [
      {
        "severity": "error",
        "code": "unknown_action",
        "path": "definition.steps.forward",
        "step": "forward",
        "message": "Piece \"http\" has no action \"sendRequest\" at version 1.0.0.",
        "alternatives": [
          "send_request"
        ],
        "blocking": true
      }
    ],
    "counts": {
      "error": 1,
      "info": 0,
      "warning": 0
    }
  }
}
```

**Every field**

### Body

- `definition` · Optional · object: The tree to judge, as you would send it to create/update. A workflow definition: its fields are in [Workflow definition](https://docs.gorunner.io/api/workflow-definition).
- `workflowId` · Optional · UUID: Judge this stored workflow’s current DRAFT when no `definition` is sent. Beside a `definition`, the draft it would replace: unpinned steps are judged at the version the update would give them. An id that names no workflow you can read is then ignored.

### Response · 200 OK

- `data` · object: The result of judging a definition. Always delivered with HTTP 200 — read `ok`, never the status code.
  - `ok` · boolean: True when nothing BLOCKING was found. Warnings and info diagnostics can be present with `ok: true` — that is the point of the distinction. A blocking diagnostic is exactly what a save or publish will refuse on.
  - `diagnostics` · array of objects: Every problem found, capped at 200.
    - `severity` · string: One of `error`, `warning`, `info`.
    - `code` · string: Stable machine-readable identifier for the kind of problem. Switch on this, not on `message`. The Code step’s own: `code_expression_in_code` (Code 2.0.0 code holds a `{{ steps…/trigger…/$… }}`, which 2.0.0 does not replace — in live code it blocks publish, never a save; inside string or template text it is a warning, since 2.0.0 keeps that text as written), `code_empty` (2.0.0 blank code, blocks publish; 1.0.0 empty code, a warning: it fails at run time), `code_v1_expression` (Code 1.0.0 pastes `{{ }}` into the program: a warning when it reads the trigger, else info), `code_unavailable_api` (`require(`, `import`, `fetch(`, `XMLHttpRequest`, `process.env`), `code_global_not_here` (a 1.0.0 `$` name in 2.0.0 code), `code_timeout_capped` (1.0.0 stops at 30 s whatever Timeout says), `code_output_path` (`steps.<code step>.<key>` where the output is `{result, logs}`), `code_returns_result` (`return { result: … }`), `code_syntax_error` (the code does not compile as its run compiles it, so every run that reaches the step fails before a line of it runs: the message names the line of the code as written; it blocks publish, never a save, and is a warning on a step no run reaches (skipped, or in error steps its step does not route to); a 1.0.0 step whose `{{ }}` are pasted in before it runs is not checked). A step on Code 1.0.0 only ever gets warnings and info, except for code that cannot compile (`code_syntax_error`) or is over 256 KB (`code_too_large`). `step_name_too_long` (a step’s `name` or `displayName` over 255 characters: a run reaching it could not save its steps. Blocking, and refused by every write; a warning for a name the definition being replaced already holds, so a workflow stored with one can still be saved and published). `missing_required_config` blocks only at an app’s default version: a step pinned to (or, unpinned, running) an older version gets it as a warning.
    - `path` · string: Where in the definition the problem is, e.g. `trigger.nextAction.config.body_type`.
    - `step` · string: The step’s `name`, when the problem belongs to a step.
    - `field` · string: The offending config field, when there is one.
    - `message` · string: Human-readable explanation.
    - `alternatives` · array of strings: The admissible values at this position, nearest miss first.
    - `moreAlternatives` · integer: How many legal values were NOT listed — so you can tell a complete vocabulary from a sample. 0 or absent means `alternatives` is the whole set.
    - `blocking` · boolean: True when a save or publish would refuse on this. Set by the validator; the informational endpoint and the write gates cannot disagree because they run the same function.
  - `counts` · object: Diagnostic counts keyed by severity (`error`, `warning`, `info`). Severities with no diagnostics are absent.
    - `{key}` · integer: Each key’s value.
  - `skipped` · array of strings: Check families that did NOT run — most often because the worker fleet is unavailable, which would otherwise make every app look unknown. A short diagnostics list plus a non-empty `skipped` is not a clean bill of health.
  - `truncated` · integer: How many further diagnostics were dropped by the 200 cap. Absent or 0 means the list is complete.

Related: [Create a workflow](https://docs.gorunner.io/api/workflows/create-workflow) · [Update a workflow’s draft](https://docs.gorunner.io/api/workflows/update-workflow) · [Review what publishing changes](https://docs.gorunner.io/api/publish/get-publish-review)

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