# Code steps through the API

A Code step runs JavaScript inside a workflow: normalising a payload, validating fields, building a CSV, computing a reward. It is often the step that carries the business logic, and it is a normal step type here: you can create it, change it, copy it, import it, test it and deploy a workflow that contains it.

This page is the shape of a Code step, how to test one without trusting it, and the two places where it still behaves differently from the rest, dry-run output and secret handling.

## What a Code step stores

[`GET /{workflowId}/nodes/{nodeId}`](https://docs.returning.ai/api-reference/workflow-builder/get-a-step.md) returns it like any other step, with the source in full:

`Response`

```json
{
  "data": {
    "node": {
      "_id": "65f1a2b3c4d5e6f708192a50",
      "name": "Normalise batch",
      "type": "code",
      "order": 1,
      "continueOnFailure": false,
      "retryOnFailure": false,
      "options": {
        "inputs": { "data": ["{{65f1a2b3c4d5e6f708192a41.data}}"] },
        "sourceCode": { "code": "export const code = async (inputs) => {\n  const raw = inputs.data;\n  const parsed = typeof raw === \"string\" ? JSON.parse(raw) : raw;\n  return { data: Array.isArray(parsed) ? parsed : [parsed] };\n};\n" }
      }
    },
    "revision": "162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c",
    "redactedPaths": []
  }
}
```

- `options.inputs` is the binding: a map of names the source receives, each value a `DynamicValue` array, so a template such as `{{<triggerId>.data}}` or `{{<stepId>.<label>}}` resolves exactly as it does in any other step.
- `options.sourceCode.code` is the source, stored as authored. The module exports `code`, an async function of `inputs`, and whatever it returns becomes the step's result, which later steps reference by name.
- The step is `sideEffects: true` in [`GET /catalog`](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md), because source can write. That flag is what a client reads to decide whether a step needs a person's attention.

Creating it is an ordinary [`POST /{workflowId}/nodes`](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) with `type: "code"`:

`Add a step`

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
  --data '{
    "type": "code",
    "name": "Normalise batch",
    "position": {
      "after": "trigger"
    },
    "options": {
      "inputs": {
        "data": ["{{65f1a2b3c4d5e6f708192a41.data}}"]
      },
      "sourceCode": {
        "code": "export const code = async (inputs) => {\n  const raw = inputs.data;\n  const parsed = typeof raw === \"string\" ? JSON.parse(raw) : raw;\n  return { data: Array.isArray(parsed) ? parsed : [parsed] };\n};\n"
      }
    }
  }'
```

Changing it is a `PATCH` on the step. `options` is replaced as a whole, so send `inputs` and `sourceCode` together:

`Update a step`

```bash
curl --request PATCH \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes/65f1a2b3c4d5e6f708192a50" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "options": {
      "inputs": {
        "data": ["{{65f1a2b3c4d5e6f708192a41.data}}"],
        "limit": ["{{65f1a2b3c4d5e6f708192a41.batch_size}}"]
      },
      "sourceCode": {
        "code": "export const code = async (inputs) => {\n  const raw = inputs.data;\n  const limit = Number(inputs.limit ?? 0);\n  const parsed = typeof raw === \"string\" ? JSON.parse(raw) : raw;\n  const rows = Array.isArray(parsed) ? parsed : [parsed];\n  return { data: limit > 0 ? rows.slice(0, limit) : rows };\n};\n"
      }
    }
  }'
```

Everything else is the ordinary route: [`POST /{workflowId}/nodes/{nodeId}/duplicate`](https://docs.returning.ai/api-reference/workflow-builder/duplicate-a-step.md) copies the step with its source, [`POST /{workflowId}/duplicate`](https://docs.returning.ai/api-reference/workflow-builder/duplicate-a-workflow.md) copies a workflow containing one, [`POST /`](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) with a `definition` imports one, and [`POST /{workflowId}/deploy`](https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow.md) takes a workflow containing one live.

## Testing a Code step

Both test routes accept a Code step. What they do with it differs, and the difference matters:

- **`mode: "live"`** (the default) runs the source for real, in the platform sandbox, with the resolved inputs. Its return value is the step's `output`. This is the only mode that proves the source works here.
- **`mode: "dry-run"`** never executes source. It reports `wouldDo.inputs` (the resolved inputs) and `wouldDo.sourceCode`, plus a warning that the output is unavailable:

`Response`

```json
{
  "data": {
    "mode": "dry-run",
    "sideEffects": "none",
    "workflowId": "65f1a2b3c4d5e6f708192a3b",
    "nodeId": "65f1a2b3c4d5e6f708192a50",
    "status": "simulated",
    "simulated": true,
    "wouldDo": {
      "inputs": { "data": [{ "email": "trader@example.com" }] },
      "sourceCode": { "code": "export const code = async (inputs) => {\n  ...\n};\n" }
    },
    "warnings": ["Code is simulated without executing source; its output is unavailable in dry-run."],
    "trace": [],
    "redactedPaths": []
  }
}
```

So a dry run of a workflow whose Code step feeds a later step cannot lift the later step: a simulated step produces no output, and the later step fails with `UNRESOLVED_SIMULATED_REFERENCE`. Use the dry run to prove the bindings resolve, and a live test of the Code step to prove the source runs.

> A live test runs production code
>
> A live test of a Code step is exactly as consequential as the source makes it: source can call an outside system with the credentials its inputs carry, so treat a live test as running production code.

## The two places a Code step is still special

**Its output is not reviewable without running it.** No mode of this API statically evaluates JavaScript. A dry run shows the inputs and the source, not the result. If you need the result before a live run, run the same source in an environment you control with the same inputs.

**Its inputs are redacted like any other step's.** A credential bound into `inputs` arrives back as `["__REDACTED__"]` in the step read, the workflow read and the dry-run `wouldDo`, with the path listed in `redactedPaths`. Echo the marker back to keep it. A credential typed directly into `sourceCode.code` is source text, not a credential field, so it is returned exactly as written: keep secrets in `inputs` and in the credential fields of the step that uses them.

## Reviewing a stored Code step

Reading a stored Code step is safe. Before a live test or a deploy, read it the way you would read any code that runs against member data:

1. [`GET /{workflowId}/nodes/{nodeId}`](https://docs.returning.ai/api-reference/workflow-builder/get-a-step.md) for the exact source and bindings, and [`GET /{workflowId}`](https://docs.returning.ai/api-reference/workflow-builder/get-a-workflow.md) to see where the step sits in the order.
2. Check the bindings first: a step reading the wrong field fails loudly, and [`POST /{workflowId}/nodes/{nodeId}/test`](https://docs.returning.ai/api-reference/workflow-builder/test-one-step.md) with `mode: "dry-run"` shows the resolved inputs without running anything.
3. Check what the source can reach. A Code step runs in the platform sandbox, but its inputs can carry a credential, and the step after it will use whatever it returns.
4. Only then live-test it, and only against a payload you chose.

## Next steps

- [`POST /{workflowId}/nodes/{nodeId}/test`](https://docs.returning.ai/api-reference/workflow-builder/test-one-step.md) with `mode: "live"` to run one Code step against a seed you control.
- [`POST /{workflowId}/test`](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md) with `mode: "dry-run"` to check that everything around it binds.
- The [webhook](https://docs.returning.ai/workflow-builder/webhook-workflow-rewards.md), [event ingest](https://docs.returning.ai/workflow-builder/event-ingest-setup-and-batch-status.md) and [trading-rewards](https://docs.returning.ai/workflow-builder/trading-rewards-from-event-ingest.md) playbooks show where a Code step fits in a reward workflow.

## Endpoints on this page

- POST [Add a step](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) `/{workflowId}/nodes`
- PATCH [Update a step](https://docs.returning.ai/api-reference/workflow-builder/update-a-step.md) `/{workflowId}/nodes/{nodeId}`
- GET [Get a step](https://docs.returning.ai/api-reference/workflow-builder/get-a-step.md) `/{workflowId}/nodes/{nodeId}`
- GET [List step types and trigger types](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) `/catalog`
- POST [Duplicate a step](https://docs.returning.ai/api-reference/workflow-builder/duplicate-a-step.md) `/{workflowId}/nodes/{nodeId}/duplicate`
- POST [Duplicate a workflow](https://docs.returning.ai/api-reference/workflow-builder/duplicate-a-workflow.md) `/{workflowId}/duplicate`
- POST [Create a workflow](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) `/`
- POST [Deploy a workflow](https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow.md) `/{workflowId}/deploy`
- GET [Get a workflow](https://docs.returning.ai/api-reference/workflow-builder/get-a-workflow.md) `/{workflowId}`
- POST [Test one step](https://docs.returning.ai/api-reference/workflow-builder/test-one-step.md) `/{workflowId}/nodes/{nodeId}/test`
- POST [Test a whole workflow](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md) `/{workflowId}/test`

Paths that start with `/v1/` are full paths; the rest are relative to `https://api.returning.ai/v1/workflow-builder/data-workflows`.
