POST /{workflowId}/validate needs workflowBuilder.warnings, errors is always empty, and valid is always true: it does not tell you whether anything was found, so read warnings. No finding here refuses a deploy: deploy runs the same check, returns the same list as warnings, and proceeds either way. Deploy refuses only on its two tenant checks, a router branch that lists a step from another workflow (400 NODE_NOT_IN_WORKFLOW) and a leaderboard from another community (400 LEADERBOARD_NOT_IN_COMMUNITY); validate reports both as warnings. Read the report, decide, and fix what matters.Authorization: Bearer <API_KEY> with workflowBuilder. No If-Match is needed because the call writes nothing: it always validates the stored workflow, and the body is ignored.| Failure | Meaning | Recovery |
|---|---|---|
403 API_KEY_PERMISSION_DENIED | The key has no workflowBuilder permission | Grant the permission. |
404 WORKFLOW_NOT_FOUND | Unknown id, or another community's workflow | Use an id from this community. |
{
"data": {
"valid": true,
"errors": [],
"warnings": [],
"revision": "162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
}
}OR condition group, plus a reference to a branch step that may not run. Both are warnings, valid is still true, and the workflow can be deployed exactly as it is:{
"data": {
"valid": true,
"errors": [],
"warnings": [
{
"nodeId": "65f1a2b3c4d5e6f708192a3e",
"path": "options.branches[0].conditions[0].operator",
"code": "ROUTER_OPERATOR_UNSAFE",
"message": "The runtime treats OR and WHERE condition groups as always true."
},
{
"nodeId": "65f1a2b3c4d5e6f708192a3d",
"path": "options.fields[0].value",
"code": "REFERENCE_MAY_BE_UNSET",
"message": "This reads a branch step that may not run; the value can be unset."
}
],
"revision": "68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"
}
}200 whenever the workflow was read and checked, whatever the report says: a workflow with warnings is a successful validation with valid: true, an empty errors and a non-empty warnings. There is no state change, so the readback is the report itself plus revision, which is stable until the next authored change.deploy returns the same list in its own warnings, so a workflow can go live with every code below present:code | Meaning |
|---|---|
NODE_OPTIONS_INVALID | The step's options do not match its type's schema. |
NODE_NAME_REQUIRED | A step has no name. |
NODE_TYPE_UNKNOWN | A stored step has a type the API does not describe. |
NODE_NOT_ON_WORKFLOW | A step exists but is not part of the workflow's order. |
NODE_NOT_IN_WORKFLOW | A router branch names a step that is not in this workflow. |
TRIGGER_TYPE_UNKNOWN | The stored trigger type is not one the API describes. |
TRIGGER_OPTIONS_INVALID | The stored trigger options do not match the trigger type. A trigger that stores no options at all is valid: an event ingest trigger takes its defaults, and a webhook trigger reads as "no options", so a workflow built in the canvas validates cleanly here. |
BRANCH_REQUIRED | A router has no branches. |
BRANCH_TYPE_INVALID | A branch is neither CONDITION nor FALLBACK. |
BRANCH_MEMBER_MISSING | A branch names a step that is not in the workflow. |
NODE_LISTED_TWICE | One step id is listed more than once across a router's branch lists, so a run would execute it more than once. |
MISSING_SECRET | A credential an import blanked is still empty, so the step will run without it. |
LEADERBOARD_NOT_IN_COMMUNITY | A stored leaderboard step names a leaderboard this community does not have. |
POST_LOGIC_REQUIRED | A write step's field does not send logic explicitly, and the default overwrite can zero a balance. |
DUPLICATE_FIELD_LABEL | Two fields in one step use the same label, so the last value wins at run time. |
EMPTY_FIELD_LABEL | A step whose output another step reads has an empty field label, so the value cannot be addressed. |
RETRY_REPEATS_SIDE_EFFECTS | retryOnFailure is set on a router whose branches contain a step that pays, sends mail or calls outside the service, or on a write step that sets more than one field. Retrying either repeats several side effects at once. A single HTTP, email, bulk or one-field write step may use retryOnFailure. |
REFERENCE_TO_SELF | A step reads its own output. |
REFERENCE_UNRESOLVED | A reference points at a step that does not run before it on its own path. |
REFERENCE_NOT_UPSTREAM | A reference points at a step that is not earlier on the same path. |
REFERENCE_UNRESOLVABLE | A reference object, or a {{...}} template whose first part is a 24-hex id, names a step that is not in this workflow. |
TEMPLATE_NOT_A_REFERENCE | A {{...}} whose first part is not a step id at all, for example {{amount}}. The run leaves the text as it is, so the value never resolves. |
REFERENCE_UNKNOWN_FIELD | A reference names a label the step it reads does not produce. |
REFERENCE_SHAPE_MISMATCH | A reference reads a number or a list as if it were an object, for example reading a field off a math step. |
ROUTER_OPERATOR_UNSAFE | A condition group uses OR or WHERE, or a condition uses the "is empty" or "is not empty" operator. The runtime treats an OR or WHERE group as always true, so the branch fires more often than the author meant. Use AND groups. |
ROUTER_OPERATOR_UNKNOWN | The router does not know the operator for that condition's type, so the condition would always be false. It is also reported for a boolean condition using is true or is false with no value. |
ROUTER_EMPTY_CONDITION | A condition branch has no conditions, so it cannot gate anything. |
ROUTER_MULTIPLE_GROUPS | A branch carries more than one condition group, and only the last one is evaluated. |
UNKNOWN_USER_FIELD | A stored platform-action field key is not one the backend computes, so the step reads or writes nothing. A key with leading or trailing spaces is reported too, because field names must match exactly. |
REFERENCE_MAY_BE_UNSET | The reference is to a branch step that may not have run, so its value can be unset. |
TRIGGER_REQUIRED | The workflow has no trigger. It deploys and never runs until one is set. |
| Status and code | When | Recovery |
|---|---|---|
400 VALIDATION_FAILED | The id is not 24 hexadecimal characters, or the body is not a JSON object | Send {} and a valid id. |
401 AUTHENTICATION_REQUIRED | No key, or an unknown key | Send a valid key. |
403 API_KEY_PERMISSION_DENIED | No workflowBuilder | Grant the permission. |
404 WORKFLOW_NOT_FOUND | Unknown id, or another community's workflow | Use an id from this community. |
415 UNSUPPORTED_MEDIA_TYPE | The body was not declared application/json | Send the header with {}. |
429 RATE_LIMITED | The community's API rate limit was reached | Wait for the window to reset, then retry. |
500 INTERNAL_ERROR | Unexpected failure | Retry with bounded backoff. |
revision between reports rather than assuming they describe the same state.valid stays true either way: check whether warnings is empty, not valid. The platform accepts every one of these and then behaves as described.MISSING_SECRET appears only for credentials that were blanked by an import. A step whose authentication is deliberately empty is not reported. Because deploy does not refuse on it, an imported workflow can go live with a credential still missing: fill every path in secretsToFill first.OR or WHERE is stored without complaint, deploys, and then evaluates as always true. Build conditions with AND groups and treat ROUTER_OPERATOR_UNSAFE as an editing task, not a blocker.validate writes nothing, so it is the cheapest way to check a workflow before you deploy or hand it over. It reaches outside the workflow only for two scoped reads: leaderboard ids and the community's user-field catalog.PATCH /{workflowId}/nodes/{nodeId} to fix a named problem.POST /{workflowId}/test with mode: "dry-run" for a side-effect-free walk of the whole workflow.POST /{workflowId}/deploy once the warnings are what you expect; its response repeats them.curl --location 'https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/validate' \
--header 'Authorization: Bearer <API_KEY>' \
--header 'Content-Type: application/json' \
--data '{}'{
"data": {
"valid": true,
"errors": [],
"warnings": [],
"revision": "162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
}
}