Code Node Reference
Exhaustive reference for the code node — inputs, outputs, utilities, and limits.
Code Node Reference
This is the exhaustive reference. If you are new to the Code node, read overview.mdx first.
Entry function
Each Code node runs one function. The handler picks which one by inspecting the source in the following order:
export default function <name>(...)— the exported name wins.function run(...)— the conventional name wins if no default export.- The first
function <name>(...)declaration anywhere in the file — fallback.
If no named function is found, the step fails with:
Code must define a named function (e.g.
function run(inputs, utils) {... })
Arrow functions assigned to variables are not discovered by the regex. Always use a function declaration.
Canonical shape
function run(inputs, utils) {
// ... your logic ...
return { /* output */ };
}The sandbox always calls fn(inputs, __utils) — the second argument is injected unconditionally, even when the handler passes null. __utils is also available as a global, so function run(inputs) and then __utils.uuid() works equally well.
Inputs
inputs is a plain JavaScript object built from the node's fieldMappings config. Each key in fieldMappings is one of your declared inputs; each value is a template expression resolved against the outputs of all completed upstream nodes.
- Keys are whatever you name them in the inspector.
- Values are resolved via the standard
{{...}}template language. A whole-template expression like{{action_1}}yields the raw upstream object; an inline one like"{{trigger.email}}"yields the interpolated string. See field mapping for the full rules. inputsis not{ trigger, steps }. There is no magicstepsortriggerkey. You get exactly what you declared in the inspector.
Example
Inspector config:
{
"fieldMappings": {
"email": "{{trigger.email}}",
"order": "{{action_1}}"
}
}Your code sees:
// inputs shape
{
email: "ada@example.com",
order: { id: "o_42", total: 9900, currency: "RUB" }
}If you need both fields from a trigger and an upstream action, declare each one separately in fieldMappings.
Outputs
The return value of your entry function becomes the node's output. Downstream nodes reference it as {{<thisNodeId>.<field>}}.
Serialization. The sandbox calls JSON.stringify(result) on the host boundary before returning. The standard JSON.stringify rules apply:
| Value | Behaviour |
|---|---|
Plain object, array, string, number, boolean, null | Preserved. |
undefined (top level) | Becomes null after round-trip. |
Object property set to undefined | Silently dropped. |
| Function | Silently dropped (as a property) or round-trips as undefined at top level. |
Symbol-keyed property | Silently dropped. |
BigInt | Throws — JSON.stringify cannot serialize BigInt. Step fails. |
Date | Converted to ISO string. You lose the Date type. |
Map, Set, RegExp | Collapse to {} (no enumerable own properties). |
| Circular reference | Throws — step fails. |
Buffer / typed arrays | Not recommended; shape is implementation-defined. Buffer is not available anyway. |
If you need to return an integer that exceeds Number.MAX_SAFE_INTEGER, return it as a string. If you want undefined fields to survive, replace them with null explicitly.
Async support
Yes. Return a Promise, or declare the function async. The sandbox detects .then on the return value and awaits it:
async function run(inputs, utils) {
const id = utils.uuid();
return { id };
}The 30-second step timeout covers async work too — it wraps the entire sandbox invocation, not just synchronous execution. If your promise never settles within 30 s, the step fails.
Note that there is no way to actually perform I/O inside the promise — fetch and the timers are gone. Async support exists so that pure-CPU work using Promise composition still resolves cleanly.
__utils — full reference
Available as global __utils and as the second function argument. Sourced from plus the dayjs injection at line 173-182.
| Name | Signature | Purpose |
|---|---|---|
dayjs | dayjs(input?): Dayjs | Full dayjs — parse, format, add/subtract time. Injected from the real library source at sandbox boot. |
uuid | uuid(): string | Generates a RFC 4122 v4 UUID using Math.random. Not cryptographically strong — use hash() for anything sensitive. |
pick | pick(obj: object, keys: string[]): object | New object with only keys that exist on obj. Missing keys are skipped. |
omit | omit(obj: object, keys: string[]): object | New object with every own enumerable key of obj except those in keys. |
groupBy | groupBy(arr: T[], key: string): Record<string, T[]> | Groups array items by String(item[key]). |
keyBy | keyBy(arr: T[], key: string): Record<string, T> | Indexes array items by String(item[key]). Later items with the same key overwrite earlier ones. |
uniqBy | uniqBy(arr: T[], key: string): T[] | Returns items from arr with unique String(item[key]); first wins. |
base64Encode | base64Encode(str: string): string | UTF-8 string → base64 string. Implemented on the host via Buffer. |
base64Decode | base64Decode(str: string): string | Base64 string → UTF-8 string. Implemented on the host via Buffer. |
hash | hash(str: string, algo?: string): string | Hex digest. algo defaults to "sha256"; any algorithm Node's crypto.createHash accepts works (e.g. "sha1", "sha512", "md5"). |
Every one of these is exercised by.
console logging
console.log, .warn, .error, .info, .debug are all installed and all captured. Each call is formatted as [<level>] <args joined by space> and pushed to an array held by the host.
- Limit: 1000 lines per execution. Once the array holds 1000 entries, subsequent calls are silently dropped — no error, no warning in your output.
- At the end of execution the handler emits one log entry at
infolevel with the captured lines under theconsoleLogsfield, so you can find them in the run journal. console.logis not part of the node's returned output. It's a sidecar stream for debugging only.
Blocked APIs
Explicitly deleted by the bootstrap script:
fetchsetTimeout,clearTimeoutsetInterval,clearIntervalsetImmediate,clearImmediate
Never present in the isolate because isolated-vm does not expose them:
processrequire(no CommonJS loader)import— neither staticimport...statements (rejected at eval time) nor dynamicimport()(no module resolver)- Filesystem APIs (
fs,pathand friends) - Network APIs (
net,http,https,dgram, DNS) Buffer(use__utils.base64Encode/Decodeinstead)- Worker threads,
child_process,cluster - Node's
cryptomodule (use__utils.hashfor digests)
Anything you expect to find on globalThis in Node but don't see here — assume it's absent and don't try to polyfill. There is no module loader to pull in a polyfill with.
Limits
| Limit | Value | Source |
|---|---|---|
| Memory per execution | 64 MB | DEFAULT_SANDBOX_CONFIG.memoryLimitMb |
| Wall-clock timeout | 30 s | DEFAULT_SANDBOX_CONFIG.timeoutMs (shared with the engine's step timeout) |
| Captured log lines | 1000 | MAX_LOGS in code-sandbox |
| Isolate reuse | None — fresh isolate per call | createCodeSandbox() + dispose() per invocation |
For the engine-wide step and pipeline timeouts that also apply, see workflow builder limits.
Errors
Any thrown error — syntax error, reference error (for example calling fetch), thrown Error, or a rejected promise — is caught by the handler and recorded as a step_failed event:
error.code: always"CODE_EXECUTION_FAILED".error.message: the thrown error's.message, orString(error)for non-Error throws.- Stack trace: the isolate stack is included in the error message when
isolated-vmsurfaces it (for example, on syntax errors and uncaught runtime errors). Visible in the run journal. step_failed_continued: if the node hascontinueOnFailureset, the run continues with this node's output set to the error payload — same as any other failing step.
The handleCodeNode unit tests confirm that throw new Error("oops") from user code lands as a step_failed with failedNodeIds updated and the handler returns true (meaning: halt this branch).
The timeout path (code runs longer than 30 s) throws from context.eval; the handler catches it the same way, and the error message comes from isolated-vm.
Related
- Overview — sandbox model, blocked APIs at a glance, when to reach for a Code node.
- n8n migration — translating n8n Function/Code nodes to Triggo.
- Field mapping — the template language for getting data into
inputs.