Test a whole workflow
Walks the workflow from its trigger and returns one trace entry per step it reached, in execution order.
- Method
- POST
- Path
https://api.returning.ai / v1/ workflow-builder/ data-workflows/ {workflowId}/ test - Permission
- workflowBuilder
- Retries
- A dry run is safe to retry: it writes no history, no step status, no member data, and makes no outbound call.
Authentication#
- Header
Authorization: Bearer <API_KEY>- Permission
- workflowBuilder
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.
The workflow must have a trigger, because the walk starts from its output. A single-step test does not have that requirement.
| 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. |
Behaviour#
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.
A trace shows which branch the run took, or would take. That is the part no single step test can answer.
Request#
Path parameters#
_id.Rule^[0-9a-fA-F]{24}$
Eg65f1a2b3c4d5e6f708192a3b
Headers#
Body#
Watch for
- The default mode is
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.- A simulated step produces no output, so a later step that reads it is not executed: it is reported as
failedwithUNRESOLVED_SIMULATED_REFERENCE, and the workflow'sstatusisfail. That is deliberate: an invented value would be worse than an obvious gap. A workflow test'sinputseeds 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 withPOST /{workflowId}/nodes/{nodeId}/testandinput, or run the workflow live. - The trigger's stored sample result is used when no
triggerPayloadis sent. Seed every test with the event shape you expect, or you are testing the sample. - A live test leaves no run in the workflow's history, so "test then read the run" is not a sequence. Deploy and fire the trigger for a real run.
- A workflow needs nothing validated to be tested:
POST /{workflowId}/validatereports the advisory findings, and neither a test nor a deploy refuses on them. scenariodoes not exist: one call tests one payload. Send several calls for several events.
More examples and details
A dry run seeded with a synthetic event:
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/test" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{"mode":"dry-run","triggerPayload":{"email":"trader@example.com","trade_id":"T-1024","volume":"2.5"}}'
A live run, which is what an omitted mode gives you: it behaves like a real run of the workflow:
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/test" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{"mode":"live","triggerPayload":{"email":"trader@example.com"}}'
A dry run that takes the condition branch: the read step executed, the write step was simulated, and the other branch was skipped.
{
"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": []
}
}
curl --request POST \
--url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/test' \
--header 'Authorization: Bearer <API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"mode": "dry-run",
"triggerPayload": {
"email": "trader@example.com",
"trade_id": "T-1024",
"volume": "2.5"
}
}'
Response#
HTTP 200 with mode, sideEffects, status (success when no step failed, fail otherwise), trace and redactedPaths.
Each trace entry carries the step id, name and type, its 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.
A live test also returns 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.
For a dry run, prove nothing changed with a read on the data the workflow would touch: the member's field value, or the last run in GET /{workflowId}/runs, must be the same before and after.
Eg{ ... }
dry-run or live.Eg"dry-run"
none for a dry run, real for a live run.Eg"none"
Eg"65f1a2b3c4d5e6f708192a3b"
success when no step failed, fail otherwise.Eg"success"
Eg[ ... ]
Eg"65f1a2b3c4d5e6f708192a3c"
Eg"Read member XP"
Eg"platform-action"
executed for a read-only step that really ran, simulated for one that would have written, skipped or failed.Eg"executed"
RuleNullable
Eg{"Total XP": 150}
Eg[]
Eg"XP > 100"
position.branchIndex and the branch path parameter count from 0.Eg1
Eg{"action": "post-user-fields"}
Eg"USER_NOT_FOUND"
Eg"User not found"
Eg[]
Eg"65f1a2b3c4d5e6f708192a46"
{
"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": []
}
}
Errors#
Fix the request06
VALIDATION_FAILEDFix the requestmode, or a body that is not a JSON object. Set a trigger first, and send a supported mode.AUTHENTICATION_REQUIREDFix the requestAPI_KEY_REQUIREDFix the requestAPI_KEY_PERMISSION_DENIEDFix the requestworkflowBuilder permission. Grant it.VALIDATION_FAILEDFix the requestUNSUPPORTED_MEDIA_TYPEFix the requestapplication/json. Send the header.Fix the data01
WORKFLOW_NOT_FOUNDFix the dataRetry with backoff02
RATE_LIMITEDRetry with backoffINTERNAL_ERRORRetry with backoffRetries
A dry run is safe to retry: it writes no history, no step status, no member data, and makes no outbound call. A live test is not safe to retry. It runs every step for real, in order, exactly like a triggered run, so a second accepted call can pay a second reward. After a timeout, read the member's history and the run list before deciding.
{
"meta": {
"status": "error",
"statusCode": 400,
"code": "VALIDATION_FAILED",
"errors": [
{
"path": "params.workflowId",
"message": "Id must be a 24 character hex string"
}
],
"requestId": "3f4d0e8b-7a6c-4f1e-9b2d-5a7c8e1f0a3b"
},
"message": "API request rejected error.",
"detail": "The request body is not valid.",
"solution": "Read error detail and try again."
}