The Script (JavaScript) block runs your code on each message. Use it when the other blocks cannot express what you need: decoding an unusual binary format, looping over a list, building a complex object. Prefer the ready blocks when they fit: they are easier to read, need no script allowance and run faster.
Settings
| Setting | Meaning | Default | Values |
|---|---|---|---|
| code | The script | an example | required; up to 64 KiB |
| outputs | Number of outputs | 1 | 1 to 4 (o0 to o3) |
| labels | Output names, comma-separated | empty | letters, digits, _, space and -; one per output, all different |
Each script block in a deployed flow counts toward your plan’s Script blocks (3, 6 or 29 by IoT tier). A deploy that contains a script is a Code-risk deploy: it asks for the flow’s name to be typed and is audited (“Script changed”).
What the code sees
The editor shows the signature over the code field: msg, node, context, flow, global, env → return msg | [a, b] | [[a, b]] | "label" | nothing.
| Name | What it is |
|---|---|
msg | The message, a copy you may change. msg.metadata is writable except the system fields; msg.msgType and msg.originator are read-only. |
node.send(msg) | Send a message on output 1 at once (node.send([a, b]) picks outputs as a return does) |
node.status({ fill, shape, text }) | The status shown under the block |
node.warn(value), node.error(text) | Write to the block’s log |
context.get("key"), context.set("key", value) | A store of this block |
flow.get(…), flow.set(…) | The flow’s variables, shared with flow.* paths in other blocks |
global.get(…), global.set(…) | The organisation’s variables (global.*) |
env.get(name) | An environment value |
console.log(value) | The same log as node.warn |
Use the key persist:name for the persistent store, for example flow.set("persist:total", 12). Setting a key to undefined deletes it.
Context writes are applied after the script has run. The first write over a store’s limit stops the rest, and the message leaves on error with limit; earlier writes stay.
What it may return
| Return | Effect |
|---|---|
return msg | Sends the message on output 1 |
return [a, b] | a on output 1, b on output 2 (up to 4); null sends nothing on that output |
return [[a, b]] | Both on output 1 |
return "alarm" | The incoming message on the output labelled alarm |
Nothing (undefined, null, []) | Nothing is sent; the message stops here |
At most 64 messages per run. A fresh object such as return { payload: 1 } is completed from the input; one that sets an engine field to something else fails with readonly_field.
Helpers
| Namespace | Functions |
|---|---|
bytes | fromHex, toHex, fromBase64, toBase64, sha256, toText, fromText, readUint, readInt, readFloat, writeUint, writeInt, writeFloat; also TextEncoder, TextDecoder |
num | round(value, digits), toHex(value, { bytes, bigEndian, prefix }) |
bits | fromBytes, fromInt, toInt, get |
obj | flatten(value, { separator, exclude, pathInKey }) |
time | parse, format, add, fields, with patterns such as yyyy-MM-dd HH:mm:ss and IANA zones |
Not available: require, import(), eval, Function, fetch, timers (use a Delay block), process, Buffer, WebAssembly. The script has no network, no files and no credentials. Date is the real clock.
Limits
| Limit | Value |
|---|---|
| CPU time per message | 50 ms (your plan may allow up to 200 ms) |
| Memory | 32 MiB |
| Code | 64 KiB |
| Messages returned | 64 |
| Message size, in or out | 256 KiB |
| Context passed in | 64 KiB |
Three kills in a row (timeout or memory) block the script for 60 seconds: messages then fail with blocked without running. The CPU time of every run counts toward the month’s Script CPU on the usage card.
Errors
The error output carries msg.error.code:
| Code | Meaning |
|---|---|
script | The code threw; line and col say where |
timeout, memory | Over the CPU time or the memory |
limit | More than 64 messages returned, or a context write over its limit |
msg_too_big | A message over 256 KiB |
unknown_label | Returned a label no output has |
bad_return | Returned something that is not a message, a list or a label (or a promise) |
readonly_field | Changed msgType, originator or a system field |
blocked | Paused after three kills |
host_busy | The sandbox stayed busy after a few tries |
script_unavailable | The script service is not available |
Test the script
Test the script in the block panel runs the code once on the real sandbox: “Nothing is deployed; Save puts the code into the block.” Fill in Payload (JSON), Metadata (JSON object), Message type, Originator type and Originator id, optionally Context (optional JSON) as { "node": {…}, "flow": {…}, "global": {…} }, and select Run. The result shows each output, the log, the context written and the CPU time. Save into the block keeps the code.
Examples
Decode a 4-byte float from a raw hex payload:
const data = bytes.fromHex(msg.payload.raw_hex);
msg.payload = { level_m: num.round(bytes.readFloat(data, 0, 4, { bigEndian: true }), 3) };
return msg;
Split one message with a list of sensors into one reading per sensor key:
// in: { "sensors": [ { "id": "t1", "v": 21.5 }, { "id": "t2", "v": 19.0 } ] }
const out = {};
for (const s of msg.payload.sensors || []) out[`temp_${s.id}`] = s.v;
msg.payload = out; // { temp_t1: 21.5, temp_t2: 19 }
return msg;
Route by a computed class to labelled outputs (labels ok,warn,crit):
const t = msg.payload.temperature;
if (typeof t !== 'number') return null;
return t > 8 ? 'crit' : t > 6 ? 'warn' : 'ok';
Keep a running total across messages:
const total = (flow.get('persist:litres_total') || 0) + (msg.payload.litres || 0);
flow.set('persist:litres_total', total);
msg.payload.litres_total = total;
return msg;
When not to use a script
| Need | Use instead |
|---|---|
| A formula | Calculate |
| Rename, copy or delete keys | Change |
| Binary fields | Bytes |
| Branching on a value | Switch or If / else |
| Consumption from a counter | Delta |
| Waiting | Delay, Trigger, Timer |
See Data transform blocks and Flow limits.

