POST /{workflowId}/test needs workflowBuilder.mode decides whether the real step services run:mode: "live" is the default and behaves like a triggered run: every step executes for real, in order, and may write, send, call outside or pay a reward.mode: "dry-run" is opt-in and side-effect free: read-only steps really run, and every step that would write, send or call is simulated with the inputs it would have used.Authorization: Bearer <API_KEY> with workflowBuilder, on a workflow of the key's community. One permission covers both modes; there is no separate live-test permission and no confirmation header, so mode is the only thing that decides whether the walk costs anything.| Failure | Meaning | Recovery |
|---|---|---|
400 VALIDATION_FAILED | The workflow has no trigger | PUT /{workflowId}/trigger first. |
404 WORKFLOW_NOT_FOUND | Unknown id, or another community's workflow | Use an id from this community. |
mode gives you: it behaves like a real run of the workflow:{
"data": {
"mode": "dry-run",
"sideEffects": "none",
"workflowId": "65f1a2b3c4d5e6f708192a3b",
"status": "success",
"trace": [
{ "nodeId": "65f1a2b3c4d5e6f708192a3c", "name": "Read member XP", "type": "platform-action", "status": "executed", "output": { "XP": 150 }, "warnings": [] },
{ "nodeId": "65f1a2b3c4d5e6f708192a3e", "name": "XP over 100?", "type": "router", "status": "executed", "warnings": [] },
{ "nodeId": "65f1a2b3c4d5e6f708192a3d", "name": "Add 10 XP", "type": "platform-action", "branch": "XP > 100", "branchIndex": 1, "status": "simulated", "wouldDo": { "action": "post-user-fields" }, "warnings": [] },
{ "nodeId": "65f1a2b3c4d5e6f708192a3f", "name": "Notify receiver", "type": "http", "branch": "Otherwise", "branchIndex": 2, "status": "skipped", "warnings": [] }
],
"redactedPaths": []
}
}200 with mode, sideEffects, status (success when no step failed, fail otherwise), trace and redactedPaths.status, and for a step inside a router the branch name and its branchIndex. branchIndex in a trace is 1-based, matching the router result the canvas shows: the first branch is 1. The write side counts from zero instead, so position.branchIndex and the branches/{branchIndex} path name the first branch as 0. A step on a branch the run did not take is skipped, which is how a trace proves the branch decision.historyId: the transient record the live run produced. The platform removes that record after the call, so it is an identifier for the run you just caused, not a history entry to read back.GET /{workflowId}/runs, must be the same before and after.| Status and code | When | Recovery |
|---|---|---|
400 VALIDATION_FAILED | No trigger, an unsupported mode, or a body that is not a JSON object | Set a trigger first, and send a supported mode. |
401 AUTHENTICATION_REQUIRED | No key, or an unknown key | Send a valid key. |
403 API_KEY_PERMISSION_DENIED | The key has no workflowBuilder permission | Grant it. |
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. |
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 a dry run with bounded backoff. |
live. An empty body therefore runs the whole workflow for real: send {"mode":"dry-run"} while you are still editing, and keep the live call for the change you mean to make.status: "success" means no step failed. It does not mean a simulated step did anything, and it is not a deploy.failed with UNRESOLVED_SIMULATED_REFERENCE, and the workflow's status is fail. That is deliberate: an invented value would be worse than an obvious gap. A workflow test's input seeds earlier step results for a single-step test, not for this walk, so it does not lift the block. Test the later step on its own with POST /{workflowId}/nodes/{nodeId}/test and input, or run the workflow live.triggerPayload is sent. Seed every test with the event shape you expect, or you are testing the sample.POST /{workflowId}/validate reports the advisory findings, and neither a test nor a deploy refuses on them.scenario does not exist: one call tests one payload. Send several calls for several events.POST /{workflowId}/validate for the advisory report deploy will repeat in its warnings.POST /{workflowId}/deploy to make the workflow live.GET /{workflowId}/runs after a real trigger, to read the run the trace predicted.curl --location 'https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/test' \
--header 'Authorization: Bearer <API_KEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
"mode": "dry-run",
"triggerPayload": {
"email": "trader@example.com",
"trade_id": "T-1024",
"volume": "2.5"
}
}'{
"data": {
"mode": "dry-run",
"sideEffects": "none",
"workflowId": "65f1a2b3c4d5e6f708192a3b",
"status": "success",
"trace": [
{
"nodeId": "65f1a2b3c4d5e6f708192a3c",
"name": "Read member XP",
"type": "platform-action",
"status": "executed",
"output": {
"Total XP": 150
},
"warnings": []
},
{
"nodeId": "65f1a2b3c4d5e6f708192a3e",
"name": "XP over 100?",
"type": "router",
"status": "executed",
"warnings": []
},
{
"nodeId": "65f1a2b3c4d5e6f708192a3d",
"name": "Add 10 XP",
"type": "platform-action",
"branch": "XP > 100",
"branchIndex": 1,
"status": "simulated",
"wouldDo": {
"action": "post-user-fields"
},
"warnings": []
},
{
"nodeId": "65f1a2b3c4d5e6f708192a3f",
"name": "Notify receiver",
"type": "http",
"branch": "Otherwise",
"branchIndex": 2,
"status": "skipped",
"warnings": []
}
],
"redactedPaths": []
}
}