Menu
Connectors
Code Executor Connector
Run custom JavaScript or TypeScript in an isolated sandbox with access to workflow context and a whitelisted set of libraries — no filesystem, no environment variables, and no real outbound network access.
On this page
What this connector is for
Runs custom JavaScript or TypeScript inside an isolated sandbox — a genuinely separate JS engine instance from the workflow server's own process, not just a restricted eval. Use it for logic that doesn't map cleanly to another connector or JSONata expression: custom transforms, conditional branching logic too complex for a single expression, building up a payload shape, etc. No connection is needed.
The sandbox has no filesystem access, no environment variables, and no real network access — see "What's available in the sandbox" below.
Execute Code
Configuring it
- Language —
javascriptortypescript. Defaults tojavascript. TypeScript is transpiled to JavaScript before running (a compilation error fails the step with the TypeScript compiler's own error message). - Code — required. Your script. Define a
main()function (sync or async — anasync function main()thatawaits something works) as the entry point, or use a direct top-levelreturnstatement; either way, whatever value comes back becomes this step's output. You mayimportfrom the whitelisted libraries below — anything else throwsLibrary "<name>" is not allowedbefore your code even runs. - Timeout (ms) — defaults to 5000. Whatever you set is capped at 30000ms (30 seconds) — a larger value is silently clamped, not rejected. This is a real wall-clock ceiling on the whole step, not just CPU-bound execution: it also bounds time spent awaiting async work inside
main()(e.g. asetTimeout-based delay), not only synchronous code. - Memory Limit (MB) — defaults to 128. Capped at 512MB regardless of what you set. Code that exceeds its isolate's memory limit is terminated.
What's available in the sandbox
- Whitelisted imports —
lodash,date-fns,uuid,zod,validator,jsonpath,jsonata,slugify,axios,jsonwebtoken. Only a curated subset of each library's functions is exposed (e.g. lodash'smap/filter/groupBy/merge/... — not the entire library surface). axiosdoes not make real HTTP requests. It's importable and lets you build request configs, but every method (get,post,put,delete,patch,request) immediately rejects with"HTTP requests not allowed in isolated VM". There is no outbound network access from inside this sandbox.context— read-only workflow data, injected without functions (to avoid clone errors) plus a few helper methods added separately:context.workflowId,context.stepId,context.executionId,context.timestamp,context.environment.context.steps— an array of prior steps ({ id, name, reference, index, body }), and each step is also exposed directly by its reference, e.g.context.getUser.body(mirrors{{getUser.body}}in other connectors).context.getStep(reference)looks one up dynamically by string.context.trigger— the normalized trigger data (context.trigger.body,.headers,.query,.params), ornullif the workflow wasn't triggered by a request.context.variables— workflow variables keyed by name (context.variables.apiKey);context.getVariable(key)is equivalent.context.variableConfigs/context.getVariableConfig(key)give the full config (type, etc.) rather than just the value.context.connections,context.connectionsById,context.connectionList— connection contexts for connections attached to this workflow.context.utils— small helpers:parseJSON(str),stringifyJSON(obj),now()(ISO string),timestamp()(epoch ms),trim(str),toLowerCase(str),toUpperCase(str).
console.log/.warn/.error/.info— available, but their output goes only to the platform's own server-side logs. It is not surfaced anywhere in the workflow builder (not in execution details, not in test results) — don't rely onconsole.logto inspect a value while building a workflow; return the value frommain()instead and read it from this step's response.- Functions can never be part of the returned result — anything you return is serialized (functions are stripped, circular references become
"[Circular]",Dates become ISO strings) before leaving the sandbox.
Reading the response
{{<stepReference>.response.data.<field>}}— whatevermain()returned (or your top-levelreturnvalue), verbatim. If you returned{ total: 42 }, use{{<stepReference>.response.data.total}}.
For example, a step named "Compute Total" (reference computeTotal) whose code does:
function main() {
const items = context.getOrderDetails.body.items;
return { total: items.reduce((sum, i) => sum + i.price * i.qty, 0) };
}
reads back as {{computeTotal.response.data.total}}.
Examples
One example per whitelisted library, each using only the functions that library actually exposes inside the sandbox (see "What's available in the sandbox" above) — not the full npm package surface.
lodash — group orders by status:
import { groupBy } from 'lodash';
function main() {
const orders = context.getOrders.body.data;
return groupBy(orders, 'status');
}
date-fns — compute a due date and how many days remain:
import { format, addDays, differenceInDays } from 'date-fns';
function main() {
const orderDate = new Date(context.getOrder.body.createdAt);
const dueDate = addDays(orderDate, 14);
return {
orderDate: format(orderDate, 'yyyy-MM-dd'),
dueDate: format(dueDate, 'yyyy-MM-dd'),
daysUntilDue: differenceInDays(dueDate, new Date()),
};
}
uuid — generate an idempotency key for a downstream request:
import { v4 as uuidv4 } from 'uuid';
function main() {
return { idempotencyKey: uuidv4() };
}
zod — validate the trigger payload's shape before using it:
import { object, string, number, safeParse } from 'zod';
function main() {
const schema = object({ email: string(), quantity: number() });
const result = safeParse(schema, context.trigger.body);
if (!result.success) {
throw new Error('Invalid payload');
}
return result.data;
}
validator — check whether a submitted email is well-formed:
import { isEmail } from 'validator';
function main() {
const email = context.trigger.body.email;
return { email, isValid: isEmail(email) };
}
jsonpath — pull every SKU out of an order's line items:
import { query } from 'jsonpath';
function main() {
return { skus: query(context.getOrder.body, '$.lineItems[*].sku') };
}
jsonata — evaluate an expression that's itself built at runtime (not something {{...}} templating alone can do):
import { evaluate } from 'jsonata';
function main() {
return { total: evaluate('$sum(items.price)', context.getOrder.body) };
}
slugify — build a URL-safe handle from a product title:
import slugify from 'slugify';
function main() {
return { handle: slugify(context.getProduct.body.title) };
}
jsonwebtoken — sign a short-lived token, using a workflow variable as the secret:
import jwt from 'jsonwebtoken';
function main() {
return {
token: jwt.sign(
{ userId: context.trigger.body.userId },
context.variables.jwtSecret,
{ expiresIn: '1h' }
),
};
}
Also applies here
Step Name
What it's for
Every step in a workflow gets a name — either one you set or a default based on the connector and action (e.g. "Get Order Details", "Send Welcome Email"). It's shown throughout the UI and in your execution history, and it's also the source for the step's Reference — a camelCase identifier auto-generated from the name (e.g. "Get Order Details" → getOrderDetails) — which is what you actually use in {{...}} expressions to read this step's output from later steps. See Step Reference.
Rules
- Must be at least 2 characters, and 50 characters or fewer.
- Must be unique within the workflow — reusing a name that's already taken will be rejected, with a suggested alternative (e.g.
"Get Order Details 2"). - Can't be empty.
Tips
- Prefer a descriptive, human-readable name over a generic one — "Get Order Details" is easier to work with later than "HTTP Request 2", especially once a workflow has a dozen steps.
- Renaming a step updates every reference to it elsewhere in the workflow automatically.
Step Reference
Syntax
Any input field can reference earlier data using {{expression}}. The expression is evaluated as JSONata — so simple dot-paths and more advanced queries (filters, functions) both work.
Referencing a step's output
Use the step's Reference — a camelCase identifier auto-generated from its Name (e.g. "Get Order Details" → getOrderDetails), shown read-only wherever the step's fields are configured — followed by the field path. Elsewhere in these docs this general pattern is written as {{<stepReference>.field.path}}:
{{getOrderDetails.response.data.id}}
{{getOrderDetails.response.status}}
The raw display name won't work here even though it's what you see in the UI — {{Get Order Details.response.data.id}} isn't valid, since a bare name containing spaces isn't a single JSONata identifier. Always use the camelCase Reference.
You can also reference steps by position instead of by reference:
{{steps[0].response.data.id}}
Referencing trigger data
{{trigger.headers.authorization}}
{{trigger.body.customerId}}
{{trigger.query.page}}
{{trigger.params.orderId}}
{{trigger.method}}
{{trigger.path}}
Referencing workflow variables
{{variables.myVariable}}
See Variables for the full list of variable types and more examples, including connection-type variables.
Referencing runtime variables
A separate, mutable namespace written by the Variable connector while a run is in progress — not the same as the workflow-level variables above:
{{runtimeVariables.myVariable}}
Notes
- If an expression can't be resolved (a typo in a step name, a field that doesn't exist), it resolves to an empty string rather than failing the workflow — check your execution history if a value comes through blank.
- Object values are automatically JSON-stringified when interpolated into a string field.
Execution Settings
Fire-and-forget
When enabled, the workflow doesn't wait for this step to complete before moving to the next one. Use it for steps whose result nothing downstream depends on — logging, analytics, notifications — so they don't add latency to the steps that matter.
Continue on error
When enabled, a failure in this step doesn't stop the workflow — execution continues to the next step. The failure is still recorded in the execution history; this just controls whether it's fatal.
Combine the two for steps that are genuinely optional to the outcome: fire-and-forget so they don't add latency, continue-on-error so a failure in them (e.g. an analytics endpoint being briefly down) doesn't take down an otherwise-successful workflow run.