Check a definition
POST/
Checks a definition and lists every problem, without saving anything.
Needs an API token with the Read permission for workflows (workflow:read).
Parameters
Body
Send at least one of the two.
definitionOptional · objectA tree you are about to save.
workflowIdOptional · UUIDA saved workflow, to check its draft. Ignored when
definitionis 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
- 200OK
The report, whether or not the definition is valid. Read
ok, not the status. The body isn’t JSON, it has neither
definitionnorworkflowId, orworkflowIdalone isn’t a UUID.- 403
FORBIDDENThe token lacks the Read permission for workflows. Checking changes nothing, so reading is enough.
- 404
NOT_FOUNDNo workflow with that
workflowIdin the workspace. Beside adefinition, such an ID is ignored. GoRunner couldn’t check which apps this workspace may use. Nothing changed. Try again.
Any call can also answer: 401 UNAUTHORIZED · 401 TOKEN_ENDED · 403 TOKEN_SURFACE_OFF · 403 FEATURE_OFF · 403 EMBED_NOT_ALLOWED · 403 EMBED_NOT_AVAILABLE · 403 ACCOUNT_SUSPENDED · 413 PAYLOAD_TOO_LARGE · 415 INVALID_CONTENT_TYPE · 429 RATE_LIMITED · 503 BUSY · 503 PAUSED · 503 SERVICE_UNAVAILABLE. See Errors.
Request
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": {}
}
}'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();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
{
"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
definitionOptional · objectThe tree to judge, as you would send it to create/update.
A workflow definition: its fields are in Workflow definition.
workflowIdOptional · UUIDJudge this stored workflow’s current DRAFT when no
definitionis sent. Beside adefinition, 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
dataobjectThe result of judging a definition. Always delivered with HTTP 200 — read
ok, never the status code.okbooleanTrue 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.diagnosticsarray of objectsEvery problem found, capped at 200.
severitystringOne of
error,warning,info.codestringStable 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’snameordisplayNameover 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_configblocks only at an app’s default version: a step pinned to (or, unpinned, running) an older version gets it as a warning.pathstringWhere in the definition the problem is, e.g.
trigger.nextAction.config.body_type.stepstringThe step’s
name, when the problem belongs to a step.fieldstringThe offending config field, when there is one.
messagestringHuman-readable explanation.
alternativesarray of stringsThe admissible values at this position, nearest miss first.
moreAlternativesintegerHow many legal values were NOT listed — so you can tell a complete vocabulary from a sample. 0 or absent means
alternativesis the whole set.blockingbooleanTrue 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.
countsobjectDiagnostic counts keyed by severity (
error,warning,info). Severities with no diagnostics are absent.{key}integerEach key’s value.
skippedarray of stringsCheck 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
skippedis not a clean bill of health.truncatedintegerHow many further diagnostics were dropped by the 200 cap. Absent or 0 means the list is complete.