Triggo Documentation
Code Node

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:

  1. export default function <name>(...) — the exported name wins.
  2. function run(...) — the conventional name wins if no default export.
  3. 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.
  • inputs is not { trigger, steps }. There is no magic steps or trigger key. 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:

ValueBehaviour
Plain object, array, string, number, boolean, nullPreserved.
undefined (top level)Becomes null after round-trip.
Object property set to undefinedSilently dropped.
FunctionSilently dropped (as a property) or round-trips as undefined at top level.
Symbol-keyed propertySilently dropped.
BigIntThrowsJSON.stringify cannot serialize BigInt. Step fails.
DateConverted to ISO string. You lose the Date type.
Map, Set, RegExpCollapse to {} (no enumerable own properties).
Circular referenceThrows — step fails.
Buffer / typed arraysNot 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.

NameSignaturePurpose
dayjsdayjs(input?): DayjsFull dayjs — parse, format, add/subtract time. Injected from the real library source at sandbox boot.
uuiduuid(): stringGenerates a RFC 4122 v4 UUID using Math.random. Not cryptographically strong — use hash() for anything sensitive.
pickpick(obj: object, keys: string[]): objectNew object with only keys that exist on obj. Missing keys are skipped.
omitomit(obj: object, keys: string[]): objectNew object with every own enumerable key of obj except those in keys.
groupBygroupBy(arr: T[], key: string): Record<string, T[]>Groups array items by String(item[key]).
keyBykeyBy(arr: T[], key: string): Record<string, T>Indexes array items by String(item[key]). Later items with the same key overwrite earlier ones.
uniqByuniqBy(arr: T[], key: string): T[]Returns items from arr with unique String(item[key]); first wins.
base64Encodebase64Encode(str: string): stringUTF-8 string → base64 string. Implemented on the host via Buffer.
base64Decodebase64Decode(str: string): stringBase64 string → UTF-8 string. Implemented on the host via Buffer.
hashhash(str: string, algo?: string): stringHex 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 info level with the captured lines under the consoleLogs field, so you can find them in the run journal.
  • console.log is not part of the node's returned output. It's a sidecar stream for debugging only.

Blocked APIs

Explicitly deleted by the bootstrap script:

  • fetch
  • setTimeout, clearTimeout
  • setInterval, clearInterval
  • setImmediate, clearImmediate

Never present in the isolate because isolated-vm does not expose them:

  • process
  • require (no CommonJS loader)
  • import — neither static import... statements (rejected at eval time) nor dynamic import() (no module resolver)
  • Filesystem APIs (fs, path and friends)
  • Network APIs (net, http, https, dgram, DNS)
  • Buffer (use __utils.base64Encode/Decode instead)
  • Worker threads, child_process, cluster
  • Node's crypto module (use __utils.hash for 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

LimitValueSource
Memory per execution64 MBDEFAULT_SANDBOX_CONFIG.memoryLimitMb
Wall-clock timeout30 sDEFAULT_SANDBOX_CONFIG.timeoutMs (shared with the engine's step timeout)
Captured log lines1000MAX_LOGS in code-sandbox
Isolate reuseNone — fresh isolate per callcreateCodeSandbox() + 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, or String(error) for non-Error throws.
  • Stack trace: the isolate stack is included in the error message when isolated-vm surfaces it (for example, on syntax errors and uncaught runtime errors). Visible in the run journal.
  • step_failed_continued: if the node has continueOnFailure set, 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.

  • 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.

On this page