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.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.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.POST /{workflowId}/nodes with type: "code":PATCH on the step. options is replaced as a whole, so send inputs and sourceCode together: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.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:{
"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": []
}
}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.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.GET /{workflowId}/nodes/{nodeId} for the exact source and bindings, and GET /{workflowId} to see where the step sits in the order.POST /{workflowId}/nodes/{nodeId}/test with mode: "dry-run" shows the resolved inputs without running anything.POST /{workflowId}/nodes/{nodeId}/test with mode: "live" to run one Code step against a seed you control.POST /{workflowId}/test with mode: "dry-run" to check that everything around it binds.