Returning.AIDocs
v1

API reference / Workflow Builder

.md

Test a whole workflow

Walks the workflow from its trigger and returns one trace entry per step it reached, in execution order.

Last updated 28 Oct 2026API v1

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.

FailureMeaningRecovery
400 VALIDATION_FAILEDThe workflow has no triggerPUT /{workflowId}/trigger first.
404 WORKFLOW_NOT_FOUNDUnknown id, or another community's workflowUse 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#

workflowId#stringREQUIRED
Workflow identifier: 24 hexadecimal characters, as returned in _id.

Rule^[0-9a-fA-F]{24}$

Eg65f1a2b3c4d5e6f708192a3b

Headers#

Authorization#stringREQUIRED
Community API key: Bearer <API_KEY>.

EgBearer <API_KEY>

Content-Type#stringREQUIRED
Request body format.

Ruleapplication/json

Body#

mode#stringOPTIONAL
live (default) runs the real step services and may write or pay rewards. dry-run is opt-in and side-effect free.

Ruledry-run, live; default live

Eg"dry-run"

triggerPayload#stringOPTIONAL

Eg{ ... }

input#objectOPTIONAL
Earlier step results keyed by step id, for a single-step test.

Eg{"65f1a2b3c4d5e6f708192a3c": {"XP": 150}}

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 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.
  • The trigger's stored sample result is used when no triggerPayload is 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}/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.
More examples and details

A dry run seeded with a synthetic event:

BASH
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:

BASH
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.

JSON
{
  "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.

data#object
A whole-workflow test result.

Eg{ ... }

mode#string
dry-run or live.

Eg"dry-run"

sideEffects#string
none for a dry run, real for a live run.

Eg"none"

workflowId#string
Workflow under test.

Eg"65f1a2b3c4d5e6f708192a3b"

status#string
success when no step failed, fail otherwise.

Eg"success"

trace#object[]
One entry per step the walk reached.

Eg[ ... ]

nodeId#string
Step identifier.

Eg"65f1a2b3c4d5e6f708192a3c"

name#string
Step name.

Eg"Read member XP"

type#string
Step type.

Eg"platform-action"

status#string
executed for a read-only step that really ran, simulated for one that would have written, skipped or failed.

Eg"executed"

output#object
Step result, for an executed step.

RuleNullable

Eg{"Total XP": 150}

warnings#string[]
Anything the caller should know about the entry.

Eg[]

branch#string
Branch name, for a step inside a router branch.

Eg"XP > 100"

branchIndex#integer
Branch index in the trace. Results are 1-based, matching the router result the canvas shows; position.branchIndex and the branch path parameter count from 0.

Eg1

wouldDo#object
Resolved inputs, for a simulated step.

Eg{"action": "post-user-fields"}

code#string
Machine code for a failure.

Eg"USER_NOT_FOUND"

message#string
Human explanation for a failure.

Eg"User not found"

redactedPaths#string[]
Credential locations replaced in this response.

Eg[]

historyId#string
Set on a live test: the transient record the run produced.

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

400VALIDATION_FAILEDFix the request
No trigger, an unsupported mode, or a body that is not a JSON object. Set a trigger first, and send a supported mode.
401AUTHENTICATION_REQUIREDFix the request
No key, or an unknown key. Send a valid key.
401API_KEY_REQUIREDFix the request
A valid admin session was used, because this API accepts community API keys only. Use a community API key.
403API_KEY_PERMISSION_DENIEDFix the request
The key has no workflowBuilder permission. Grant it.
413VALIDATION_FAILEDFix the request
The request body is too large to read. Nothing was written. Send a smaller body; a large import is better split into a create plus step writes.
415UNSUPPORTED_MEDIA_TYPEFix the request
The body was not declared application/json. Send the header.

Fix the data01

404WORKFLOW_NOT_FOUNDFix the data
Unknown id, or another community's workflow. Use an id from this community.

Retry with backoff02

429RATE_LIMITEDRetry with backoff
The community's API rate limit was reached. Wait for the window to reset, then retry.
500INTERNAL_ERRORRetry with backoff
Unexpected failure. Retry a dry run with bounded backoff.

Retries

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."
}

Next step#

Deploy a workflowPOST/v1/workflow-builder/data-workflows/{workflowId}/deploy