# Test one step

Runs one step.

- Endpoint: `POST https://api.returning.ai/v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId}/test`
- Section: Workflow Builder / Tests and deploy
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `workflowBuilder`
- Retries: A dry run is safe to retry: it writes nothing and calls nothing.
- Guide: generated from the published specification
- Last updated: 28 Oct 2026
- Web page: https://docs.returning.ai/api-reference/workflow-builder/test-one-step

## 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. What protects you is `mode`, so send `"dry-run"` explicitly while you are still editing.

No `If-Match` is needed: neither mode changes the workflow configuration.

| Failure | Meaning | Recovery |
| --- | --- | --- |
| `403 API_KEY_PERMISSION_DENIED` | The key has no `workflowBuilder` permission | Grant the permission. |
| `404 NODE_NOT_FOUND` | The step is not part of this workflow | Read the workflow for its step ids. |

## Behaviour

`mode` decides whether the real step services are called:

- `mode: "live"` is the **default**, and matches the canvas Test button. The step really runs: it may write member fields, send mail, call an outside system or pay a reward.
- `mode: "dry-run"` is **opt-in** and side-effect free. Read-only steps really run; everything that would write, send or call is resolved and reported instead of executed.

Both modes share one response shape, and `sideEffects` states which one you got: `"real"` or `"none"`.

## Request

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `workflowId` | `string` | Yes | Workflow identifier: 24 hexadecimal characters, as returned in `_id`. (`^[0-9a-fA-F]{24}$`) |
| `nodeId` | `string` | Yes | Step identifier: 24 hexadecimal characters, as returned in the step's `_id`. (`^[0-9a-fA-F]{24}$`) |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key: `Bearer <API_KEY>`. |
| `Content-Type` | `string` | Yes | Request body format. (`application/json`) |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | `string` | No | `live` (default) runs the real step services and may write or pay rewards. `dry-run` is opt-in and side-effect free. (`dry-run`, `live`; default `live`) |
| `input` | `object` | No | Earlier step results keyed by step id, for a single-step test. |
| `triggerPayload` | `string` | No | - |

### Watch for

- The default is `live`. An empty body therefore runs the step for real. Send `{"mode":"dry-run"}` while you are still editing.
- `wouldDo` is built from redacted options, so a credential appears as `["__REDACTED__"]` and its path is listed in `redactedPaths`. A dry run never reveals a stored credential.
- A downstream step's references resolve only from `input` (keyed by step id) and `triggerPayload`. Without a seed, a value resolves to nothing and the step may report a failure that would not happen in a run.
- A simulated step produces no output. In a workflow test a later step that reads it fails with `UNRESOLVED_SIMULATED_REFERENCE`; testing that step directly with `input` is how you cover it.
- A leaderboard step reads the leaderboard's own community, so a dry run fails it before any read if the leaderboard is not the key's community's.
- A Code step is tested like any other step. A dry run does not execute its source: it reports `wouldDo.sourceCode` and a warning that the output is unavailable, because a simulated step produces no output. A live test runs the source for real.
- A live node test does not create a run in the workflow's history; it runs the step and returns the result. The whole-workflow live test is the one that behaves like a run.

### Example request

```bash
curl --request POST \
  --url 'https://api.returning.ai/v1/workflow-builder/data-workflows/<workflowId>/nodes/<nodeId>/test' \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "mode": "dry-run"
  }'
```

### More examples

A dry run of a write step, seeded with a trigger payload so the references resolve:

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes/65f1a2b3c4d5e6f708192a3d/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 dry run of a downstream step, seeded with the earlier step's result:

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes/65f1a2b3c4d5e6f708192a3d/test" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"mode":"dry-run","input":{"65f1a2b3c4d5e6f708192a3c":{"XP":150}}}'
```

A live run, which is what you get when you omit `mode`. It may write and pay:

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes/65f1a2b3c4d5e6f708192a3d/test" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{"mode":"live","triggerPayload":{"email":"trader@example.com"}}'
```

A simulated write step: nothing was written, and `wouldDo` carries the resolved inputs.

```json
{
  "data": {
    "mode": "dry-run",
    "sideEffects": "none",
    "workflowId": "65f1a2b3c4d5e6f708192a3b",
    "nodeId": "65f1a2b3c4d5e6f708192a3d",
    "status": "simulated",
    "simulated": true,
    "wouldDo": {
      "action": "post-user-fields",
      "selectUser": { "type": "email", "value": "trader@example.com" },
      "users": [{ "id": 98765, "_id": "65f1a2b3c4d5e6f708192a60" }],
      "fields": [{ "label": "XP", "field": "xp", "logic": "add", "value": "10" }]
    },
    "warnings": [],
    "trace": [{ "nodeId": "65f1a2b3c4d5e6f708192a3d", "name": "Add 10 XP", "type": "platform-action", "status": "simulated", "warnings": [] }],
    "redactedPaths": []
  }
}
```

## Response

HTTP `200` with a single result object.

- `sideEffects` is `"none"` for a dry run and `"real"` for a live run. Check it before reading anything else.
- `status` is `executed` for a read-only step that really ran, `simulated` for a step that would have written, `skipped` or `failed`.
- `wouldDo` is the resolved input for a simulated step: who would be written, which field, which amount and which logic; the request that would be sent for an HTTP step, with credentials redacted; the row count and a sample for a bulk step; the recipient count and template for an email step.
- `output` is present only for an executed step.
- `trace` has one entry for a normal step. A router test walks the branches its conditions select, so it returns one entry per step on the path it would take.

A dry run proves nothing was written. Confirm that with a read: `GET /runs/{runId}` on the workflow's latest run before and after must be the same run, and the member's field value must be unchanged.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `data` | `object` | - | One step test result. |
| `data.mode` | `string` | - | `dry-run` or `live`. |
| `data.sideEffects` | `string` | - | `none` for a dry run, `real` for a live run. |
| `data.workflowId` | `string` | - | Workflow under test. |
| `data.nodeId` | `string` | - | Step under test. |
| `data.status` | `string` | - | `executed`, `simulated`, `skipped` or `failed`. |
| `data.simulated` | `boolean` | - | True when nothing was written, sent or called. |
| `data.wouldDo` | `object` | - | Resolved inputs the step would have used. |
| `data.warnings` | `string[]` | - | Anything the caller should know about the result. |
| `data.trace` | `object[]` | - | One entry per step the test walked. |
| `data.trace.nodeId` | `string` | - | Step identifier. |
| `data.trace.name` | `string` | - | Step name. |
| `data.trace.type` | `string` | - | Step type. |
| `data.trace.status` | `string` | - | `executed` for a read-only step that really ran, `simulated` for one that would have written, `skipped` or `failed`. |
| `data.trace.warnings` | `string[]` | - | Anything the caller should know about the entry. |
| `data.trace.branch` | `string` | - | Branch name, for a step inside a router branch. |
| `data.trace.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. |
| `data.trace.output` | `object` | - | Step result, for an executed step. (Nullable) |
| `data.trace.wouldDo` | `object` | - | Resolved inputs, for a simulated step. |
| `data.trace.code` | `string` | - | Machine code for a failure. |
| `data.trace.message` | `string` | - | Human explanation for a failure. |
| `data.redactedPaths` | `string[]` | - | Credential locations replaced in this response. |
| `data.output` | `object` | - | Step result, for an executed step. (Nullable) |

### Example response (200)

```json
{
  "data": {
    "mode": "dry-run",
    "sideEffects": "none",
    "workflowId": "65f1a2b3c4d5e6f708192a3b",
    "nodeId": "65f1a2b3c4d5e6f708192a3d",
    "status": "simulated",
    "simulated": true,
    "wouldDo": {
      "action": "post-user-fields",
      "selectUser": {
        "type": "email",
        "value": "trader@example.com"
      },
      "users": [
        {
          "id": 98765,
          "_id": "65f1a2b3c4d5e6f708192a60"
        }
      ],
      "fields": [
        {
          "label": "XP",
          "field": "xp",
          "logic": "add",
          "value": "10"
        }
      ]
    },
    "warnings": [],
    "trace": [
      {
        "nodeId": "65f1a2b3c4d5e6f708192a3d",
        "name": "Add 10 XP",
        "type": "platform-action",
        "status": "simulated",
        "warnings": []
      }
    ],
    "redactedPaths": []
  }
}
```

## Errors

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `VALIDATION_FAILED` | `mode` is neither `dry-run` nor `live`, or the body is not a JSON object. Send a supported mode. |
| 401 | `AUTHENTICATION_REQUIRED` | No key, or an unknown key. Send a valid key. |
| 401 | `API_KEY_REQUIRED` | A valid admin session was used, because this API accepts community API keys only. Use a community API key. |
| 403 | `API_KEY_PERMISSION_DENIED` | The key has no `workflowBuilder` permission. Grant the permission. |
| 413 | `VALIDATION_FAILED` | 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. |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | The body was not declared `application/json`. Send the header. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `NODE_NOT_FOUND` | The step is not part of this workflow. Read the workflow for its step ids. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 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 in dry-run mode only. |

**Retries:** A dry run is safe to retry: it writes nothing and calls nothing. Repeat it as often as you like while you are still editing. A live run is not safe to retry. It can pay a reward or send an email, and a second accepted call can pay twice. Treat a timeout as an unknown outcome: read the member's field history or the run list before deciding whether to run it again. That is the reason the dry run exists: use it for every iteration, and keep the live call for the last one.

## Next step

- [Test a whole workflow](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md): `POST /v1/workflow-builder/data-workflows/{workflowId}/test`.
