Skip to main content

JavaScript Module

Run inline JavaScript inside a Process to compute values, transform payloads, branch on logic that the standard Tasks don't cover, and — when explicitly enabled — make controlled HTTP calls to allow-listed hosts.

The JavaScript Module provides a single scripting Task. Scripts run in an isolated sandbox, see the full Session data, and return when execution finishes. HTTP access is off by default and opt-in per Environment.

What you can build

  • Compute derived variables before downstream Tasks read them — totals, hashes, formatted strings.
  • Reshape Session data — flatten nested API responses, rename keys, drop fields.
  • Conditional logic too fine-grained for SwitchTask — set a routeKey variable that a downstream Gateway then branches on.
  • Inline data validation — set context.error = '...' and let the next Gateway route to an error path.
  • One-off string parsing — split a phone number into country/local parts, extract values from a free-text user reply.
  • Targeted HTTP calls — fetch JSON from an allow-listed API and merge the response into context.

Quick start

  1. In the Process Editor, drop a Task on the canvas and convert it to JSTask.

  2. Open the JavaScript Code panel and paste:

    var qty = context.quantity;
    var price = context.unitPrice;
    context.total = qty * price;
    log('Computed total ' + context.total);
  3. Connect the JSTask to the rest of the Process. Downstream Tasks can now read context.total.

  4. (Optional) To allow HTTP calls, open the Module's Environment configuration, flip HTTP Enabled on, and add the hosts you want scripts to reach to HTTP Allowed Hosts — e.g. api.example.com, *.partner.com.

  5. Deploy the Process and run it. Confirm context.total appears on the resulting Session and the audit log carries the log() line.

Done.

How it works

When the Token reaches a JSTask, the platform:

  1. Creates a fresh, isolated script environment for that single run — no state leaks between runs, and scripts cannot reach the file system, the platform's internals, or other Sessions.
  2. Binds four globals into the script's scope:
    • context — the Session's shared variables (read & write).
    • session — a narrow view of the running Session (Channel, language, routing output).
    • log(msg) — appends a ModuleLog (4013) entry to the audit trail.
    • httpFetch(url, opts) — only callable when HTTP Enabled is on; rejects every host not in HTTP Allowed Hosts.
  3. Executes the script synchronously, enforcing the platform Limits.
  4. On any failure, sets the Task's output to error so the Sequence Flow labeled error is taken (see Failure modes).

Scripts run in strict mode: assigning to an undeclared variable throws, and eval(...) / new Function(string) are blocked. Modern JavaScript syntax is supported — let/const, arrow functions, template literals, classes, destructuring, spread, Map/Set, optional chaining (?.), and nullish coalescing (??).

Configuration

Configuration is per Environment — each Environment has its own HTTP switch and allow-list. Both fields live in the Module's Environment configuration.

FieldTypeRequiredDescription
HTTP EnabledbooleanNoMaster switch for the httpFetch(url, opts) script binding. Default: off. When off, every httpFetch call throws an error the script can try/catch. Leave off for Environments that should never make outbound network calls.
HTTP Allowed HostsstringNoComma-separated list of hostnames scripts may call, e.g. api.example.com, *.partner.com, hooks.slack.com. Default: empty (= nothing allowed). Hosts not on the list are rejected before any connection is attempted. Wildcards: *.example.com matches api.example.com and foo.example.com but not the bare example.com — list both if you need both.

Limits

These platform-protection limits are fixed and apply to every script:

  • Scripts stop after 4 seconds of execution.
  • A script may run at most 1,000 statements.
  • Recursion depth is capped at 4,000 nested calls.
  • Script memory is capped at 16 MB.
  • A single regular-expression match aborts after 1 second (protects against catastrophic backtracking).
  • Each httpFetch call times out after 3 seconds, and is clamped so it can never outlive the script timeout. Scripts may pass opts.timeoutMs to lower the timeout for a single call — never to raise it.
  • An HTTP response body is truncated at 256 KB; the returned object then has truncated: true.
  • A script may make at most 5 httpFetch calls per run.
  • Requests to private or internal network addresses are always blocked, even for allow-listed hosts.

Exceeding any execution limit aborts the script and routes the Task to its error Sequence Flow.

Tasks

JSTask (Generic)

Executes a JavaScript snippet inside the sandbox. Works on all Channels.

Use it for: computing derived variables, reshaping Session data, fine-grained conditional logic, inline validation, controlled outbound HTTP.

Properties

PropertyTypeRequiredExampleDescription
ScriptstringYescontext.total = context.qty * context.unitPrice;The JavaScript code to execute. Edited via a syntax-highlighted code editor in the side panel.

Bindings exposed to the script

BindingKindPurpose
contextobjectThe Session's shared variables. Read and write values that other Tasks see as {name}.
sessionobjectNarrow view of the running Session — see session reference.
log(msg)functionWrites a ModuleLog (4013) entry to the audit trail.
httpFetch(url, opts)functionSynchronous HTTP request. Throws a catchable error on policy violation or transport failure. Disabled by default — see Configuration.

session reference

Read-only properties (the platform sets these; scripts cannot change them):

PropertyTypeMeaning
session.IdstringThe Session's unique identifier.
session.ChannelstringE.g. "Chat", "TwilioSms", "AculabVoice".
session.ChannelSessionIdstringE.g. a Twilio conversation ID or a voice call ID.
session.ChannelSenderstringOriginating participant identifier on this Channel.
session.ChannelRecipientstringReceiving participant identifier on this Channel.
session.ContextPayloadobjectSame object as context — kept for older scripts.

Read/write properties (scripts may set these):

PropertyTypeEffect
session.UserLanguagestringIETF language tag (e.g. "en-US").
session.TaskOutputstringThe Sequence Flow label the next Gateway should pick after this Task. Set this for domain branches ('approved', 'rejected'); the platform sets it to 'error' automatically when the Task fails.

Methods:

MethodEffect
session.Log(msg)Same as the global log(msg) — writes a ModuleLog audit entry.

session deliberately exposes nothing else — no workspace identifiers, no raw payloads, no internal execution state. Prefer context for ordinary variable I/O.

httpFetch(url, opts) reference

var resp = httpFetch(url, opts);

Arguments

ArgumentTypeRequiredExampleDescription
urlstringYes'https://api.example.com/users'Absolute URL with http:// or https:// scheme. The host must be in HTTP Allowed Hosts. URLs containing user info (https://user:pw@host/) are rejected.
optsobjectNo{ method: 'POST', body: '{}' }Request options (table below). Omit for a default GET.

opts fields

FieldTypeDefaultExampleNotes
methodstring'GET''POST'One of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Case-insensitive.
headersobject{}{ 'Content-Type': 'application/json' }{ name: value } map. Values are coerced to string. The headers Host, Content-Length, Connection, Transfer-Encoding, Upgrade, Expect, Authorization, and Cookie cannot be set from a script — setting one throws. Content-* headers (Content-Type, Content-Encoding, Content-Disposition, …) require a non-null body; setting one without body throws.
bodystringnullJSON.stringify({ amount: 42 })Request body. Strings only — serialize objects with JSON.stringify(...) first.
timeoutMsnumber30001500Per-call wall-clock cap. Can only lower the 3-second platform limit; higher values are clamped down silently.

Redirects are not followed. A 3xx response is returned to the script as-is. If you want to follow a redirect, read resp.headers.location and call httpFetch again — each call gets its own allow-list check.

Return value

{
status: 200, // HTTP status code (number)
ok: true, // true iff status is 200..299
headers: {
'content-type': 'application/json', // single-valued → string
'set-cookie': ['a=1; Path=/', 'b=2; Path=/'] // multi-valued → string[]
},
body: '{"id":42,"name":"Widget"}', // string decoded with the response's charset (UTF-8 default), capped at 256 KB
elapsedMs: 142, // request duration (number)
truncated: false // true if body was capped at 256 KB
}

Header keys are lowercased. Headers with a single value are exposed as string; headers seen more than once (notably Set-Cookie) are exposed as string[].

Errors thrown (every one is catchable in JS via try/catch)

ReasonMessage starts with
HTTP Enabled is offhttpFetch: disabled.
More than 5 calls in one runhttpFetch: too many calls in one script (max 5).
First arg not a stringhttpFetch: first argument must be a string URL.
Second arg not an objecthttpFetch: second argument must be an options object.
Empty / missing URLhttpFetch: url is required.
URL won't parsehttpFetch: invalid URL '...'.
Scheme not http/httpshttpFetch: scheme '...' not allowed (use http or https).
URL contains user infohttpFetch: URLs with user info (https://user:pass@host/) are not allowed.
Host not in allow-listhttpFetch: host '...' is not in 'HTTP Allowed Hosts'.
Method not allowedhttpFetch: method '...' not allowed.
body not a stringhttpFetch: opts.body must be a string (or null).
Restricted request headerhttpFetch: header '...' cannot be set from a script.
Content-* header without a bodyhttpFetch: header '...' requires a non-null body. Pass opts.body (use '' for an empty body).
Host resolves only to private addresseshttpFetch: host '...' has no public address; rejected (SSRF guard).
Timeout (request)httpFetch: timed out after Nms.
Timeout (response read)httpFetch: response read timed out.
Network / TLS / DNS failurehttpFetch: transport error: ...

Failure modes

If the script does not catch an error, the Task ends with output error so a downstream Gateway can route to the Sequence Flow labeled error.

FailureAudit codeAudit message starts with
Empty ScriptTaskInvalidData (3005)JSTask: Script is required.
Syntax / parse errorTaskInvalidData (3005)JSTask: parse error at line …
Uncaught JS throw / TypeErrorModuleException (4001)JSTask: runtime error at line …
Script timeout (4 s)ModuleException (4001)JSTask: script timed out after …
Statement-count overflowModuleException (4001)JSTask: exceeded Max Statements …
Recursion-depth overflowModuleException (4001)JSTask: exceeded Recursion Depth Limit …
Memory-limit overflowModuleException (4001)JSTask: exceeded Memory Limit …
Uncaught httpFetch rejectionModuleException (4001)JSTask: runtime error at line …: httpFetch: …
External cancellationModuleException (4001)JSTask: execution cancelled.

Example

A minimal end-to-end scenario:

  1. An InputTask stores the user's reply in context.quantity; an earlier SetVariableTask put context.unitPrice = 12.5 in the Session.
  2. A JSTask runs with Script = context.total = context.quantity * context.unitPrice; log('Total ' + context.total);.
  3. With quantity = 3, the Session now has total = 37.5 and the audit trail shows 4013 ModuleLog | Total 37.5.
  4. A downstream OutputTask renders Your total is {total}.

More complete scenarios — including HTTP calls with realistic responses — are in Worked examples.

Scripting patterns

Read & write Session variables

context is the Session's shared scratchpad. Anything an upstream Task wrote is readable; anything you write is visible to downstream Tasks.

// Read upstream values
var firstName = context.user && context.user.firstName;
var cart = context.cart || [];

// Compute and write back
var subtotal = 0;
for (var i = 0; i < cart.length; i++) {
subtotal += cart[i].price * cart[i].qty;
}
context.subtotal = subtotal;
context.tax = subtotal * 0.21;
context.total = subtotal + context.tax;

After this Task runs, downstream OutputTasks can use {subtotal}, {tax}, {total} in their text templates.

Tip — defensive reads. Strict mode treats reading an undefined property as undefined (not an error), but calling a method on undefined does throw. Guard with if (context.x) { … } before drilling deeper.

Log audit messages

log(msg) writes a ModuleLog (4013) entry against the current Session. The line shows up in the audit grid for that run, useful for debugging without interrupting execution.

log('Cart had ' + (context.cart || []).length + ' items');
log('Resolved language to ' + (session.UserLanguage || 'none'));

// Conditional debug
if (!context.userId) {
log('WARN: userId missing — downstream Tasks may fall back to the anonymous path');
}

log() does not fail the Task — it is purely informational. For a recoverable error that should branch the Process, throw an exception or set context.error and let a Gateway downstream route.

Branch the Process

Throwing or setting TaskOutput are both valid ways to influence the Gateway downstream.

// Soft branch — set a flag for a SwitchTask to read
if (context.user && context.user.tier === 'premium') {
context.routeKey = 'premium';
} else {
context.routeKey = 'standard';
}

// Hard branch — fail the JSTask so it routes to its 'error' Sequence Flow
if (!context.user) {
throw new Error('userId missing'); // ends Task with output "error"
}

For domain-meaningful branches like approved vs rejected (not "error"), prefer setting session.TaskOutput directly:

session.TaskOutput = (context.score >= 0.8) ? 'approved' : 'rejected';

Call an HTTP API

Prerequisite: in the Module's Environment configuration, turn HTTP Enabled on and add api.example.com to HTTP Allowed Hosts.

var resp = httpFetch('https://api.example.com/users/' + context.userId, {
method: 'GET',
headers: { Accept: 'application/json' },
timeoutMs: 3000
});

if (!resp.ok) {
log('user lookup failed: ' + resp.status + ' ' + resp.body);
context.userLookupFailed = true;
return;
}

var user = JSON.parse(resp.body);
context.userName = user.name;
context.userEmail = user.email;

POST JSON

var payload = {
type: 'order',
amount: context.total,
currency: 'EUR'
};

var resp = httpFetch('https://api.example.com/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': context.partnerApiKey
},
body: JSON.stringify(payload),
timeoutMs: 2500
});

if (!resp.ok) {
// catchable: throw a domain error the Process can branch on
context.orderError = resp.status + ' ' + resp.body;
throw new Error('order failed');
}

context.orderId = JSON.parse(resp.body).id;
info

The Authorization and Cookie headers cannot be set from a script. Use APIs that accept a custom header (e.g. X-Api-Key) or a URL-based token, such as webhook endpoints.

Fall back when an API is down

Wrap httpFetch in try/catch to handle policy or transport failures without failing the whole Task.

try {
var resp = httpFetch('https://api.partner.com/exchange-rate?base=' + context.currency, {
timeoutMs: 1500
});
if (resp.ok) {
context.fxRate = JSON.parse(resp.body).rate;
} else {
context.fxRate = 1.0; // safe fallback
log('exchange rate API returned ' + resp.status + '; using 1.0');
}
} catch (e) {
context.fxRate = 1.0;
log('exchange rate API failed: ' + e.message + '; using 1.0');
}

Handle truncated responses

var resp = httpFetch('https://api.example.com/large-feed');

if (resp.truncated) {
log('feed response truncated at ' + resp.body.length + ' bytes');
// Responses are capped at 256 KB — request less data (server-side filters,
// pagination) or process what arrived
}

// Parsing JSON from a truncated body throws — guard
try {
context.feed = JSON.parse(resp.body);
} catch (e) {
context.feed = [];
log('feed JSON parse failed: ' + e.message);
}

Read response headers

var resp = httpFetch('https://api.example.com/things');

// Header keys are always lowercased
var rateLimit = resp.headers['x-rate-limit-remaining'];
if (rateLimit && parseInt(rateLimit, 10) < 5) {
log('warning: rate-limit headroom is ' + rateLimit);
}

var contentType = resp.headers['content-type'] || '';
if (contentType.indexOf('application/json') === 0) {
context.data = JSON.parse(resp.body);
} else {
context.data = resp.body;
}

Worked examples

Each example shows realistic Session state — what's in context (and session) when the JSTask is reached, the script, the actual API response shape where httpFetch is involved, the resulting context, and the audit-log line log() emits.

Example 1 — E-commerce cart total

Scenario: an upstream cart-builder Task assembled the basket and a coupon code; this JSTask computes subtotal, discount, VAT, and total before the next step renders an order summary.

Before — context:

{
"currency": "EUR",
"user": { "id": 4471, "tier": "premium", "country": "IT" },
"cart": [
{ "sku": "WID-100", "name": "Widget", "qty": 2, "unitPrice": 12.50 },
{ "sku": "GIZ-220", "name": "Gizmo", "qty": 1, "unitPrice": 49.00 },
{ "sku": "SHP-001", "name": "Shipping", "qty": 1, "unitPrice": 4.99 }
],
"couponCode": "SUMMER10"
}

Script:

var cart = context.cart || [];
var subtotal = 0;
for (var i = 0; i < cart.length; i++) {
subtotal += cart[i].qty * cart[i].unitPrice;
}

// 10% off when the SUMMER10 coupon is present
var discount = (context.couponCode === 'SUMMER10') ? subtotal * 0.10 : 0;
var taxBase = subtotal - discount;
var tax = taxBase * 0.22; // 22% Italian VAT

function round2(n) { return Math.round(n * 100) / 100; }
context.subtotal = round2(subtotal);
context.discount = round2(discount);
context.tax = round2(tax);
context.total = round2(taxBase + tax);

log('Cart totals: subtotal=' + context.subtotal +
' discount=' + context.discount +
' tax=' + context.tax +
' total=' + context.total);

After — context (added fields):

{
"subtotal": 78.99,
"discount": 7.90,
"tax": 15.64,
"total": 86.73
}

Audit log:

4013 ModuleLog | Cart totals: subtotal=78.99 discount=7.9 tax=15.64 total=86.73

Example 2 — Normalize a Twilio inbound message

Scenario: the Session runs on the Twilio SMS Channel. The Twilio Module populated session.Channel, session.ChannelSessionId, session.ChannelSender, session.ChannelRecipient, and put the message body into context.lastMessageBody. The JSTask flattens those into a single context.message record other Tasks can consume without reaching into session.

Before — session (read-only, set by the platform):

FieldValue
Channel"TwilioSms"
ChannelSessionId"CHabcd1234567890ef..." (conversation ID)
ChannelSender"+393331112233" (the user's number)
ChannelRecipient"+12025551234" (your Twilio number)
UserLanguage"it-IT"

Before — context:

{
"lastMessageBody": " Vorrei prenotare per 4 persone domani sera "
}

Script:

context.message = {
channel: session.Channel,
conversation: session.ChannelSessionId,
fromE164: session.ChannelSender,
toE164: session.ChannelRecipient,
language: session.UserLanguage,
text: (context.lastMessageBody || '').trim(),
receivedAt: new Date().toISOString()
};

log('Inbound ' + context.message.channel +
' from ' + context.message.fromE164 +
' length=' + context.message.text.length);

After — context (added field):

{
"message": {
"channel": "TwilioSms",
"conversation": "CHabcd1234567890ef...",
"fromE164": "+393331112233",
"toE164": "+12025551234",
"language": "it-IT",
"text": "Vorrei prenotare per 4 persone domani sera",
"receivedAt": "2026-04-27T14:32:11.483Z"
}
}

Audit log:

4013 ModuleLog | Inbound TwilioSms from +393331112233 length=42

Example 3 — Branch on classifier intent score

Scenario: an upstream OpenAI Module classifier Task wrote an intent object into context. This JSTask routes the Process to one of four paths based on the intent's label and confidence score. The diagram's Gateway has outgoing Sequence Flows labeled human, cancellation, billing, and general.

Before — context:

{
"userMessage": "Voglio annullare il mio abbonamento",
"intent": {
"label": "cancel_subscription",
"score": 0.91,
"alternatives": [
{ "label": "billing_question", "score": 0.06 },
{ "label": "general", "score": 0.03 }
]
}
}

Script:

var i     = context.intent || {};
var score = i.score || 0;
var label = i.label || 'general';

if (score < 0.6) {
// Low confidence — escalate to a human agent
session.TaskOutput = 'human';
log('Intent low-confidence (' + score + ') → human handoff');
} else if (label === 'cancel_subscription') {
session.TaskOutput = 'cancellation';
log('Routing to cancellation (intent=' + label + ' score=' + score + ')');
} else if (label === 'billing_question') {
session.TaskOutput = 'billing';
log('Routing to billing (intent=' + label + ' score=' + score + ')');
} else {
session.TaskOutput = 'general';
log('Routing to general (intent=' + label + ' score=' + score + ')');
}

After — session.TaskOutput: "cancellation" (the Gateway picks the matching outgoing Sequence Flow).

Audit log:

4013 ModuleLog | Routing to cancellation (intent=cancel_subscription score=0.91)

Example 4 — Aggregate a list into stats

Scenario: an upstream Task fetched a page of support tickets. The JSTask reduces the list to summary stats that an OutputTask renders into a Slack daily-digest message.

Before — context:

{
"tickets": [
{ "id": 1001, "status": "open", "priority": "high", "assignee": "alice" },
{ "id": 1002, "status": "open", "priority": "low", "assignee": "bob" },
{ "id": 1003, "status": "resolved", "priority": "high", "assignee": "alice" },
{ "id": 1004, "status": "open", "priority": "medium", "assignee": "alice" },
{ "id": 1005, "status": "pending", "priority": "high", "assignee": "bob" }
]
}

Script:

var tickets = context.tickets || [];
var stats = {
total: tickets.length,
byStatus: {},
byAssignee: {},
highPriorityOpen: 0
};

for (var i = 0; i < tickets.length; i++) {
var t = tickets[i];
stats.byStatus[t.status] = (stats.byStatus[t.status] || 0) + 1;
stats.byAssignee[t.assignee] = (stats.byAssignee[t.assignee] || 0) + 1;
if (t.priority === 'high' && t.status === 'open') stats.highPriorityOpen++;
}

context.stats = stats;
log('Tickets ' + stats.total +
' (open=' + (stats.byStatus.open || 0) +
' resolved=' + (stats.byStatus.resolved || 0) +
' high&open=' + stats.highPriorityOpen + ')');

After — context.stats:

{
"total": 5,
"byStatus": { "open": 3, "resolved": 1, "pending": 1 },
"byAssignee": { "alice": 3, "bob": 2 },
"highPriorityOpen": 1
}

Audit log:

4013 ModuleLog | Tickets 5 (open=3 resolved=1 high&open=1)

Example 5 — Geocode an address (httpFetch GET)

Scenario: the user provided a free-text address. The JSTask geocodes it via OpenStreetMap's Nominatim service, then writes lat/lon back for a downstream Task to use.

Required Environment configuration:

FieldValue
HTTP Enabledon
HTTP Allowed Hostsnominatim.openstreetmap.org

Before — context:

{
"user": { "id": 4471 },
"address": "Via Roma 15, 20121 Milano, Italy"
}

Script:

var q = encodeURIComponent(context.address);

var resp = httpFetch(
'https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + q,
{
method: 'GET',
headers: { 'Accept': 'application/json' },
timeoutMs: 2500
}
);

if (!resp.ok) {
context.geocodeError = 'http_' + resp.status;
log('geocode failed: ' + resp.status);
return;
}

var hits = JSON.parse(resp.body);
if (hits.length === 0) {
context.geocodeError = 'no_match';
return;
}

context.location = {
lat: parseFloat(hits[0].lat),
lon: parseFloat(hits[0].lon),
displayName: hits[0].display_name
};

log('Geocoded "' + context.address + '" → ' +
context.location.lat + ',' + context.location.lon +
' in ' + resp.elapsedMs + 'ms');

Realistic Nominatim response (resp.body):

[
{
"place_id": 305126716,
"lat": "45.4660628",
"lon": "9.1903617",
"display_name": "15, Via Roma, Brera, Centro Storico, Milano, Lombardia, 20121, Italia",
"class": "place",
"type": "house",
"importance": 0.4101
}
]

After — context (added field):

{
"location": {
"lat": 45.4660628,
"lon": 9.1903617,
"displayName": "15, Via Roma, Brera, Centro Storico, Milano, Lombardia, 20121, Italia"
}
}

Audit log:

4013 ModuleLog | Geocoded "Via Roma 15, 20121 Milano, Italy" → 45.4660628,9.1903617 in 218ms

Example 6 — Notify a Slack channel via incoming webhook (httpFetch POST)

Scenario: the order from Example 1 was placed. The JSTask posts a notification to a Slack incoming webhook (the secret token is part of the webhook URL, so no Authorization header is needed) and routes the Task to its error Sequence Flow if Slack rejects the message.

Required Environment configuration:

FieldValue
HTTP Enabledon
HTTP Allowed Hostshooks.slack.com

Before — context:

{
"user": { "id": 4471, "name": "Maria Rossi" },
"currency": "EUR",
"total": 86.73,
"orderId": "ord_2026-04-27-0042",
"slackWebhookUrl": "https://hooks.slack.com/services/T000AAA/B000BBB/XXXXXXXXXXXXXXXXXXXXXXXX"
}

Push the webhook URL into context with a SetVariableTask earlier in the Process — never hard-code it in the JSTask script.

Script:

var payload = {
text: 'New order ' + context.orderId +
' from ' + context.user.name +
': ' + context.total + ' ' + context.currency
};

var resp = httpFetch(context.slackWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
timeoutMs: 2500
});

if (!resp.ok) {
context.notify = { ok: false, httpStatus: resp.status, body: resp.body };
log('Slack notification failed ' + resp.status + ': ' + resp.body);
throw new Error('notification failed'); // routes JSTask to its "error" Sequence Flow
}

context.notify = { ok: true };
log('Slack notification sent for ' + context.orderId + ' in ' + resp.elapsedMs + 'ms');

Realistic Slack response (resp.body):

ok

After — context.notify:

{
"ok": true
}

Audit log:

4013 ModuleLog | Slack notification sent for ord_2026-04-27-0042 in 187ms

Failure path — when Slack returns 404 with body no_service (webhook revoked), the script throws and the JSTask ends with:

4013 ModuleLog       | Slack notification failed 404: no_service
4001 ModuleException | JSTask: runtime error at line 18 col 3: notification failed

…and the Task output is error, so the Gateway routes to the failure branch.


Example 7 — Exchange-rate lookup with try/catch fallback

Scenario: convert the cart total from EUR into the user's currency. The exchange-rate API is best-effort — if it fails or times out, the Process must keep going with fxRate = 1.0 rather than fail the whole Session.

Required Environment configuration:

FieldValue
HTTP Enabledon
HTTP Allowed Hostsopen.er-api.com

Before — context:

{
"amount": 86.73,
"fromCurrency": "EUR",
"toCurrency": "USD"
}

Script:

context.fxRate   = 1.0;          // safe default
context.fxSource = 'fallback';

try {
var resp = httpFetch(
'https://open.er-api.com/v6/latest/' + encodeURIComponent(context.fromCurrency),
{ timeoutMs: 1500 }
);

if (resp.ok) {
var data = JSON.parse(resp.body);
if (data.result === 'success' && data.rates && data.rates[context.toCurrency]) {
context.fxRate = data.rates[context.toCurrency];
context.fxSource = 'er-api:' + data.time_last_update_utc;
} else {
log('fx api ok but no rate for ' + context.toCurrency + '; using 1.0');
}
} else {
log('fx api returned ' + resp.status + '; using 1.0');
}
} catch (e) {
// httpFetch throws on timeout, transport errors, allow-list rejection, etc.
log('fx api failed: ' + e.message + '; using 1.0');
}

context.amountInTarget = Math.round(context.amount * context.fxRate * 100) / 100;

log('Converted ' + context.amount + ' ' + context.fromCurrency +
' → ' + context.amountInTarget + ' ' + context.toCurrency +
' @ ' + context.fxRate + ' (' + context.fxSource + ')');

Realistic er-api response (resp.body):

{
"result": "success",
"provider": "https://www.exchangerate-api.com",
"time_last_update_unix": 1714190401,
"time_last_update_utc": "Sun, 27 Apr 2026 00:00:01 +0000",
"base_code": "EUR",
"rates": {
"USD": 1.0723,
"GBP": 0.8531,
"JPY": 165.42
}
}

After — context (added fields, success path):

{
"fxRate": 1.0723,
"fxSource": "er-api:Sun, 27 Apr 2026 00:00:01 +0000",
"amountInTarget": 92.99
}

After — context (timeout / allow-list / outage path — same shape, fallback values):

{
"fxRate": 1.0,
"fxSource": "fallback",
"amountInTarget": 86.73
}

Audit log (success):

4013 ModuleLog | Converted 86.73 EUR → 92.99 USD @ 1.0723 (er-api:Sun, 27 Apr 2026 00:00:01 +0000)

Audit log (fallback after timeout):

4013 ModuleLog | fx api failed: httpFetch: timed out after 1500ms.; using 1.0
4013 ModuleLog | Converted 86.73 EUR → 86.73 USD @ 1 (fallback)

The Token continues to the next Task either way — the Process doesn't break when the exchange-rate API is having a bad day.

Common recipes

Reshape an API response before replying to the user

Call an API with the Standard Module's HttpTask, flatten the JSON with a JSTask, and render the result with an OutputTask.

  1. InputTask (Standard Module) — Store Variable orderId. The user types their order number.

  2. HttpTask (Standard Module) — Url https://api.example.com/orders/{orderId}, Method GET, Output Variable orderResponse.

  3. JSTask — Script:

    var order = JSON.parse(context.orderResponse);
    context.orderSummary = order.items.length + ' items, ' +
    order.total + ' ' + order.currency + ', status: ' + order.status;
  4. OutputTask (Standard Module) — Message Your order: {orderSummary}.

Validate a user reply and re-prompt on bad input

Check a free-text reply with a JSTask, confirm when it's valid, and ask again when it isn't.

  1. InputTask (Standard Module) — Store Variable guestCount. The user types how many people the booking is for.

  2. JSTask — Script:

    var n = parseInt(context.guestCount, 10);
    if (n >= 1 && n <= 12) {
    context.guests = n;
    session.TaskOutput = 'valid';
    } else {
    session.TaskOutput = 'invalid';
    }
  3. Exclusive Gateway — outgoing Sequence Flows labeled valid and invalid.

  4. OutputTask (valid branch) — Message Table for {guests} — confirmed!.

  5. OutputTask (invalid branch) — Message Please enter a number between 1 and 12., then loop back to the InputTask.

Best practices

  1. Keep scripts short and single-purpose — one computation per JSTask. Scripts stop after 4 seconds and 1,000 statements, so heavy processing belongs in dedicated Tasks, not inline code.
  2. Guard every read — upstream Tasks may not have set the variable you expect. Use if (context.x) { … } or var v = context.x || fallback; before drilling into nested objects.
  3. Wrap httpFetch in try/catch when the call is best-effort — a timeout or allow-list rejection then becomes a fallback path instead of failing the whole Task.
  4. Prefer context over session for variable I/O — session is for Channel metadata and routing output only.
  5. Use log() liberally while building — each call writes an audit entry you can read in the Session details, which is far faster than guessing why a branch was taken.
  6. Set session.TaskOutput for domain branches — reserve thrown errors (and the error Sequence Flow) for genuine failures, not for routing decisions like approved / rejected.

Troubleshooting

"JSTask: script timed out after 4s."

The script ran longer than the 4-second limit. If the bulk of the time is in httpFetch, lower opts.timeoutMs so a slow API doesn't eat the whole budget. Otherwise split the work across multiple Tasks or move heavy processing out of inline script.

"JSTask: exceeded Max Statements limit (1000)."

The script ran more than 1,000 statements. Common cause: an unintended infinite loop. Simplify the script, process fewer items per run, or split the work across multiple JSTasks.

"JSTask: exceeded Recursion Depth Limit (4000)."

A recursive function went deeper than 4,000 nested calls. Convert the recursion to iteration.

"JSTask: exceeded Memory Limit (16 MB)."

The script tried to allocate more than 16 MB — usually a runaway string concatenation, a large array, or a deeply nested object. Reduce allocations: process data in smaller pieces and avoid building giant intermediate strings.

"JSTask: parse error at line N col M: …"

The JavaScript Code field doesn't parse as JavaScript. Modern syntax is supported, including let/const, arrow functions, template literals, destructuring, and spread. Common cause: stray }, unterminated string, or copy-pasted source from a non-JS language.

"JSTask: runtime error at line N col M: …"

The script threw or hit a runtime error. The line/col point at the throwing statement. Common causes:

  • Reading a context.<var> that the upstream Task didn't set — guard with if (context.x) { … }.
  • Calling eval(...) or new Function('...') — both are blocked. Use plain JavaScript instead of dynamic code generation.
  • An uncaught httpFetch rejection — wrap calls in try/catch if you want to handle them, or check resp.ok and branch.

"httpFetch: disabled."

HTTP Enabled is off in the JavaScript Module's Environment configuration. Flip it on and add the host to HTTP Allowed Hosts.

"httpFetch: host '…' is not in 'HTTP Allowed Hosts'."

The host the script tried to call isn't on the allow-list. Add it in the Module's Environment configuration as a literal hostname (api.example.com) or as a wildcard (*.example.com to match any subdomain). Note that *.example.com does not match the bare example.com — list both if you need both.

"httpFetch: host '…' has no public address; rejected (SSRF guard)."

The host resolved only to private or internal network addresses. Requests to private networks are always blocked — this cannot be turned off. Point the script at a publicly reachable host.

"httpFetch: timed out after Nms."

The remote service didn't respond within the timeout. httpFetch calls are capped at 3 seconds; if you passed a lower opts.timeoutMs, raise it back toward that cap, or wrap the call in try/catch and fall back.

"httpFetch: too many calls in one script (max 5)."

A script may make at most 5 httpFetch calls per run. Batch the calls server-side where possible, or split the work across separate JSTasks.

"httpFetch: header '…' cannot be set from a script."

The script tried to set a restricted header. Host, Content-Length, Connection, Transfer-Encoding, Upgrade, Expect, Authorization, and Cookie cannot be set from scripts. For authenticated APIs, use a custom header such as X-Api-Key, or an endpoint that carries its token in the URL (like Slack incoming webhooks).

Error codes

CodeNameDescription
3005TaskInvalidDataThe Script field is empty or fails to parse. See full details
4001ModuleExceptionRuntime error, timeout, statement / recursion / memory overflow, uncaught httpFetch failure, or cancellation. See full details
4013ModuleLogInformational entry written by every log(msg) call. See full details