Returning.AIDocs
v1

Guides / Workflow Builder

.md

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} returns it like any other step, with the source in full:

Response
{
  "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, 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 with type: "code":

POSTAdd a stepcURL
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:

PATCHUpdate a stepcURL
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 copies the step with its source, POST /{workflowId}/duplicate copies a workflow containing one, POST / with a definition imports one, and POST /{workflowId}/deploy 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
{
  "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 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} for the exact source and bindings, and GET /{workflowId} 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 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

Endpoints on this page