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:
{
"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.inputsis the binding: a map of names the source receives, each value aDynamicValuearray, so a template such as{{<triggerId>.data}}or{{<stepId>.<label>}}resolves exactly as it does in any other step.options.sourceCode.codeis the source, stored as authored. The module exportscode, an async function ofinputs, and whatever it returns becomes the step's result, which later steps reference by name.- The step is
sideEffects: trueinGET /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":
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:
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'soutput. This is the only mode that proves the source works here.mode: "dry-run"never executes source. It reportswouldDo.inputs(the resolved inputs) andwouldDo.sourceCode, plus a warning that the output is unavailable:
{
"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:
GET /{workflowId}/nodes/{nodeId}for the exact source and bindings, andGET /{workflowId}to see where the step sits in the order.- Check the bindings first: a step reading the wrong field fails loudly, and
POST /{workflowId}/nodes/{nodeId}/testwithmode: "dry-run"shows the resolved inputs without running anything. - 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.
- Only then live-test it, and only against a payload you chose.
Next steps
POST /{workflowId}/nodes/{nodeId}/testwithmode: "live"to run one Code step against a seed you control.POST /{workflowId}/testwithmode: "dry-run"to check that everything around it binds.- The webhook, event ingest and trading-rewards playbooks show where a Code step fits in a reward workflow.
Endpoints on this page
- Add a step:
POST /v1/workflow-builder/data-workflows/{workflowId}/nodes - Update a step:
PATCH /v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId} - Get a step:
GET /v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId} - List step types and trigger types:
GET /v1/workflow-builder/data-workflows/catalog - Duplicate a step:
POST /v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId}/duplicate - Duplicate a workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/duplicate - Create a workflow:
POST /v1/workflow-builder/data-workflows - Deploy a workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/deploy - Get a workflow:
GET /v1/workflow-builder/data-workflows/{workflowId} - Test one step:
POST /v1/workflow-builder/data-workflows/{workflowId}/nodes/{nodeId}/test - Test a whole workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/test