JS Code
Overview
JS Code allows you to execute Javascript code inside your flow.
It is necessary to use return statement to return value, returned value is assigned to property defined in targetPath.
It is possible to access doc node variables inside JS Code step, however, here double curly braces are NOT used. It is not possible to modify doc nodes from step within.
Workspace and Environment variables can be accessed under $env field. Headers are also available under $headers.
The native Node.js crypto and Buffer modules are available as $crypto and $Buffer — for UUIDs, hashing, HMAC, encryption, and base64/hex encoding. See Cryptography and binary data.
Configuration
Code block (codeBlock)- javascript code block, the return value will be assigned to thedocnodeTarget path (targetPath)- thedocnode on which to store the return value
Step execution configuration
It is possible to configure step behaviour in runtime through environment variables:
DISABLE_CODE_IVM_STEP
If true - disables JS Code step globally
Cryptography and binary data ($crypto / $Buffer)
The JS Code step exposes the full native Node.js crypto and Buffer modules as two globals:
$crypto— the Nodecryptomodule (hashing, HMAC, random values, UUIDs, ciphers, key derivation, signing, constants, …)$Buffer— the NodeBufferclass (binary data; base64/hex/utf8 encode & decode)
You can generate UUIDs, hash and sign data, encrypt and decrypt payloads, and base64-encode/decode — directly inside a flow, with no external service or extra step.
The one rule: return a primitive
The value you return is copied out of the sandbox, so it must be a primitive — finish every crypto/Buffer chain with an encoding call such as .digest("hex"), .digest("base64"), or .toString("hex").
$Buffer/$crypto object failsA raw Buffer or crypto object cannot be copied out — the target node is left empty.
// ❌ Wrong — returns a crypto object, target ends up empty
return $crypto.createHmac("sha256", "key");
// ✅ Correct — returns a string
return $crypto.createHmac("sha256", "key").update("body").digest("hex");
(A plain object/array of primitives — e.g. return { pad: $crypto.constants.RSA_PKCS1_PADDING }; — copies out fine; only raw Buffer/proxy objects fail.)
Passing arguments
Method arguments are flexible — you can pass a $crypto/$Buffer call any of:
- a
string,number, orboolean; - a native
Uint8Array(new Uint8Array([...])); - another
$crypto/$Buffervalue — e.g.d.setAuthTag(c.getAuthTag()),$crypto.createCipheriv(algo, $Buffer.from(keyHex, "hex"), iv),$Buffer.concat([$a, $b]), or aKeyObject. These are passed by reference and rebuilt on the host.
The one argument you cannot pass is a function — callbacks are unsupported (see below).
How it works
$crypto / $Buffer are host-bridged proxies over the actual Node modules running in the runtime process. Flow code runs inside an isolated sandbox; the crypto objects live on the host side of that boundary. Reading a property or calling a method crosses to the host, runs the real function there (with the host-side object as this), and returns a primitive or a new proxy. Because intermediate objects (Hash, Hmac, Cipher, Decipher, KeyObject) stay on the host, their internal Buffer state never has to cross — which is why ciphers and hashes work. Only two things cross: arguments (in) and the return value (out).
- Async / callback / Promise APIs —
pbkdf2(…, cb),randomFill, asyncgenerateKeyPair, asyncscrypt. A callback is a function, and functions can't cross the boundary. Use the*Syncvariants (pbkdf2Sync,scryptSync,generateKeyPairSync). - Streaming interfaces — using a cipher/hash as a stream or
.pipe(). Use the one-shot.update().final()/.update().digest()form. - Returning a raw
Buffer/proxy — end with.toString(...)/.digest(...)instead.
Exposing the whole native modules does not widen your security exposure:
- You author and review the code in each
JS Codestep. The flow developer is responsible for the code they write. - Once a flow is published to production it is immutable — its code is fixed and never changes at runtime. There is no mechanism for an end user or incoming request to inject new code into a published flow.
A flow you have reviewed and published is a known, fixed quantity. As long as you develop a secure flow, full access to the native crypto / Buffer modules introduces no additional security risk.
$crypto / $Buffer reflect the runtime's actual Node.js crypto / Buffer — not a pinned shim. Available algorithms, defaults, and behaviour track the Node.js version the runtime runs on; a runtime Node upgrade can add, remove, or change these APIs. Avoid depending on behaviour specific to one Node release.
$crypto / $Buffer are available only in the JS Code (code-ivm) step, and only when your code references them. They are not available in inline {{ }} template expressions or in JSONata.
Supported operations
All of the following are verified working (synchronous APIs only):
| Category | Examples | Notes |
|---|---|---|
| Hashing | createHash("sha256" | "sha512" | "md5" | …).update(x).digest("hex" | "base64") | native parity |
| HMAC | createHmac("sha256", key).update(x).digest("hex") | key may be a string or KeyObject |
| Random | randomBytes(n).toString("hex"), randomUUID() | randomUUID is a v4 UUID |
| Symmetric — CBC | createCipheriv("aes-256-cbc", key, iv) / createDecipheriv(...) | key/iv as string or $Buffer/Uint8Array |
| Symmetric — GCM | aes-256-gcm + getAuthTag() / setAuthTag() | pass the tag proxy straight into setAuthTag |
| Key derivation | pbkdf2Sync(pw, salt, iter, len, "sha256"), scryptSync(pw, salt, len) | finish with .toString("hex") |
| Buffer encode/decode | $Buffer.from(str, "utf8" | "hex" | "base64"), .toString(...), .length, .concat([...]) | .concat takes $Buffers or Uint8Arrays |
| Keys | createSecretKey(bytes), createPrivateKey(pem), createPublicKey(pem) | the KeyObject can be passed into createHmac/sign |
| Asymmetric sign | sign("sha256", data, pemKey).toString("base64") | PEM string or KeyObject key + Uint8Array/$Buffer data |
| Introspection / constants | getHashes(), getCiphers(), $crypto.constants.* | arrays of strings / numeric constants |
Examples
to access doc nodes that have dashes in their names use the following syntax: globalThis['node-with-dashes']
Calculate sum of two numbers
Let's say we want to calculate the total amount for an order - we can do that using multiplication in JS Code step
Input
{
"price": 120,
"amount": 2
}
Step configuration
Code block (codeBlock)- the Javascript code to be executed:return price * amountTarget path (targetPath)-docnode on which to assign the result:sum
Example flow
tags: []
steps:
- stepType: code-ivm
config:
codeBlock: return price * amount
targetPath: sum
name: JS Code
description: ""
condition: ""
sampleData:
price: 120
amount: 2
Result
{
"doc": {
"price": 120,
"amount": 2,
"sum": 240
},
"errors": [],
"performance": {
"steps": [
{
"step": "code-ivm",
"executionTime": 13
}
],
"executionTimeOfFlow": 14,
"timeMetric": "ms"
}
}
Using environment variables
Let's say we want to access environment variable that stores custom URL base and make a full address inside JS Code step.
Input
{
"query": "11",
"urlEnd": "/policy/?v="
}
Prerequisites
- In environment variables,
URL_BASEvariable is set tohttps://example.com.
Step configuration
Code block (codeBlock)- the Javascript code to be executed:const adr = $env.URL_BASE + urlEnd + query; return adr;Target path (targetPath)-docnode on which to assign the result:result
Result
{
"doc": {
"query": "11",
"urlEnd": "/policy/?v=",
"result": "https://example.com/policy/?v=11"
},
"errors": []
}
Example flow
tags: []
steps:
- stepType: code-ivm
config:
codeBlock: const adr = $env.URL_BASE + urlEnd + query; return adr;
targetPath: result
name: JS Code
description: ""
condition: ""
Generate a UUID
Generate an RFC 4122 v4 UUID — useful for correlation IDs, idempotency keys, or record identifiers.
Code block (codeBlock):return $crypto.randomUUID();Target path (targetPath):id
Example flow
tags: []
steps:
- stepType: code-ivm
config:
codeBlock: return $crypto.randomUUID();
targetPath: id
name: JS Code
description: ""
condition: ""
Result
{
"doc": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
},
"errors": []
}
Hash a value (SHA-256)
Produce a hex digest with crypto.createHash.
Code block (codeBlock):return $crypto.createHash("sha256").update("abc").digest("hex");Target path (targetPath):hash
Result: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
Sign a message with HMAC
Create a keyed signature with crypto.createHmac. Keep the secret in an environment variable.
Code block (codeBlock):return $crypto.createHmac("sha256", $env.HMAC_SECRET).update(payload).digest("hex");Target path (targetPath):signature
(payload is a doc field; HMAC_SECRET is an environment variable.)
Generate a secure random token
Use crypto.randomBytes for a cryptographically strong token. Encode the bytes to a string before returning.
Code block (codeBlock):return $crypto.randomBytes(16).toString("hex");Target path (targetPath):token
Result: a 32-character hex string, e.g. "9f86d081884c7d659a2feaa0c55ad015".
Base64 and hex encode / decode
Encode and decode data with $Buffer. $Buffer.from(str, [encoding]) reads a string; .toString(encoding) writes it back out.
// encode to base64
return $Buffer.from("user:secret").toString("base64"); // "dXNlcjpzZWNyZXQ="
// decode base64 back to text
return $Buffer.from("aGVsbG8=", "base64").toString("utf8"); // "hello"
// byte length (counts bytes, not code points)
return $Buffer.from("héllo").length; // 6
// concatenate binary chunks — $Buffer values or native Uint8Arrays
return $Buffer.concat([$Buffer.from("ab"), $Buffer.from("cd")]).toString("utf8"); // "abcd"
Encrypt and decrypt data (AES-256-CBC)
Symmetric encryption with crypto.createCipheriv / createDecipheriv. The key and IV may be passed as strings or as $Buffer values.
A 32-character ASCII string is a 32-byte key; a 16-character ASCII string is a 16-byte IV:
// encrypt
var key = "0123456789abcdef0123456789abcdef"; // 32 bytes
var iv = "abcdef9876543210"; // 16 bytes
var c = $crypto.createCipheriv("aes-256-cbc", key, iv);
return c.update("secret message", "utf8", "hex") + c.final("hex");
// decrypt (same key/iv); `cipherHex` is a doc field
var key = "0123456789abcdef0123456789abcdef";
var iv = "abcdef9876543210";
var d = $crypto.createDecipheriv("aes-256-cbc", key, iv);
return d.update(cipherHex, "hex", "utf8") + d.final("utf8");
When the key is truly binary, pass a $Buffer built from its hex (both key and IV cross the sandbox fine as $Buffer values):
var key = $Buffer.from($env.AES_KEY_HEX, "hex"); // 64 hex chars → 32 bytes
var iv = $Buffer.from($env.AES_IV_HEX, "hex"); // 32 hex chars → 16 bytes
var c = $crypto.createCipheriv("aes-256-cbc", key, iv);
return c.update("secret", "utf8", "hex") + c.final("hex");
Authenticated encryption (AES-256-GCM)
AES-256-GCM produces an auth tag that decryption must verify. When encrypt and decrypt happen in the same step, pass the tag proxy straight from getAuthTag() into setAuthTag() — no serialization needed.
Code block (codeBlock):
var key = $Buffer.from($env.AES_KEY_HEX, "hex"); // 32 bytes
var iv = $Buffer.from($env.AES_IV_HEX, "hex"); // 12 bytes (GCM standard)
// --- encrypt ---
var c = $crypto.createCipheriv("aes-256-gcm", key, iv);
var ct = c.update("top secret", "utf8", "hex") + c.final("hex");
var tag = c.getAuthTag(); // $Buffer value — pass it straight in
// --- decrypt (verifies the tag) ---
var d = $crypto.createDecipheriv("aes-256-gcm", key, iv);
d.setAuthTag(tag);
return d.update(ct, "hex", "utf8") + d.final("utf8"); // → "top secret"
Target path (targetPath):plaintext
If encryption and decryption happen in separate steps, the tag can't travel as a Buffer through the doc — serialize it (getAuthTag().toString("hex")) in the first step and rebuild it in the second ($Buffer.from(tagHex, "hex")).
Hash a password (PBKDF2 / scrypt)
Use the synchronous key-derivation functions and finish with .toString("hex").
return $crypto.pbkdf2Sync($env.PASSWORD, "salt", 100000, 32, "sha256").toString("hex");
return $crypto.scryptSync($env.PASSWORD, "salt", 32).toString("hex");
Sign data with an RSA key (asymmetric)
Keys are PEM strings; data is a native Uint8Array (or $Buffer); the signature comes out as a string.
var sig = $crypto.sign("sha256", new Uint8Array([104, 105]), $env.RSA_PRIVATE_KEY_PEM);
return sig.toString("base64");
Passing binary between steps
Binary values cannot live in the doc as Buffers — only JSON-serialisable data survives. Carry them as hex or base64 strings between steps, and decode inside the step that needs bytes:
[JS Code: encrypt] → doc.cipherHex (string) → [JS Code: decrypt] reads doc.cipherHex
Prefer hex for binary interchange — it is trivial to rebuild in-sandbox with $Buffer.from(hex, "hex").
Combined example flow
This flow exercises several $crypto / $Buffer operations across steps.
tags: []
isPrimary: true
steps:
- name: uuid
stepType: code-ivm
description: ""
condition: ""
config:
codeBlock: return $crypto.randomUUID();
targetPath: uuid
- name: hash
stepType: code-ivm
description: ""
condition: ""
config:
codeBlock: return $crypto.createHash("sha256").update("abc").digest("hex");
targetPath: hash
- name: hmac
stepType: code-ivm
description: ""
condition: ""
config:
codeBlock: return $crypto.createHmac("md5", "secret-key").update("message").digest("hex");
targetPath: hmac
- name: base64
stepType: code-ivm
description: ""
condition: ""
config:
codeBlock: return $Buffer.from("abc").toString("hex");
targetPath: buf
- name: basic-auth-and-signature
stepType: code-ivm
description: ""
condition: ""
config:
codeBlock: >-
var basic = "Basic " + $Buffer.from("u:p").toString("base64");
var sig = $crypto.createHmac("sha256", "p").update("body").digest("base64");
return basic + "|" + sig;
targetPath: auth
- name: crypto-constants
stepType: code-ivm
description: ""
condition: ""
config:
codeBlock: |-
return {
rsaPadding: $crypto.constants.RSA_PKCS1_PADDING,
saltMax: $crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN
};
targetPath: constants