Scripts

Scripts run your own JavaScript on the Virtuino Cloud server. A script reads values from one or more fields, does whatever calculation or logic you need, and writes the results to other fields — without changing the firmware of your boards. This guide shows how to set one up, what your code can use, and eleven tested examples you can copy.

Contents

  1. What a script does
  2. Creating a script
  3. IO mapping: connecting fields to variables
  4. Writing the code: inputs and outputs
  5. When a script runs
  6. What the code can use, and the limits
  7. Testing and troubleshooting
  8. Examples

1. What a script does

Every script follows the same pattern: read the fields mapped as inputs, calculate, and write to the fields mapped as outputs. Typical jobs:

A script's results are ordinary field values: they appear on dashboards, can be sent to boards via MQTT, and can trigger rules — see the Data Flow guide for how they travel through the platform.

2. Creating a script

Scripts belong to a dashboard. Open the dashboard and choose Tools → Scripts.

  1. Add script opens the script library. Pick a ready-made script as a starting point.
  2. Settings — give it a name and a description, tick active status, and optionally turn on Periodic Execution (Timer) with an interval in seconds.
  3. IO mapping — connect fields to the variable names your code uses (Section 3).
  4. Edit code — write the code, try it with Run Test, and save.
Let the AI Assistant write it. Describe what you need — "convert tank/temp_raw from 0–4095 to −50…150 °C into tank/temperature" — and the assistant can create the script and its mapping for you. Review the code and mapping before you rely on it.

The icon in front of each script in the list shows its state:

IconMeaning
GreenActive and ready — the script runs.
AmberActive, but the IO mapping is incomplete — the script does not run until the mapping is fixed (Section 3).
GreyNot active — switched off in the settings.
runtime errorThe last run failed (an exception or the time limit). The note disappears after the next successful run.

3. IO mapping: connecting fields to variables

A script never names fields in its code. Instead, the mapping table links each field to a short JS variable name. The same code can then be reused with other fields just by changing the mapping. Each row has:

ColumnWhat it does
IOIN the script reads this field. OUT the script can write to this field.
FieldThe field, by its full name — e.g. kitchen/temperature.
JS VariableThe name used in the code: inputs.temp for an IN row, outputs.heater for an OUT row.
trg (IN only)A new value on this field starts the script.
pub (OUT only)A value the script writes is also sent via MQTT to the field's subscribers — tick it when a board must receive the result, e.g. a relay command.

When trg is ticked, two more options appear under the row:

A mapping is complete — and the script can run — only when: Otherwise the script stays amber and never runs, even with active status ticked.

4. Writing the code: inputs and outputs

Your code sees two objects, named after the JS variables of the mapping:

inputs — what the script reads

Every IN row becomes inputs.<variable> with two properties:

PropertyContent
inputs.temp.valueThe field's latest value. For the field that started the script, it is the new value that just arrived.
inputs.temp.timeWhen that value was recorded, as an ISO text in UTC — e.g. 2026-09-26T08:15:30.000Z.
Always convert values before calculating. A value often arrives as text — "23.5", not 23.5 — and a field that has never received anything gives an empty value. Use parseFloat() and stop if the result is not a number:
const temp = parseFloat(inputs.temp.value);
if (isNaN(temp)) return;      // no value yet, or not a number
Avoid Number() for this check: Number("") is 0, so an empty field would look like a real zero.

outputs — what the script writes

Assign a value to outputs.<variable> for each OUT row you want to write:

outputs.heater = 1;                        // a number
outputs.status = "OK";                      // or text
outputs.power  = { value: 575.2, time: inputs.amps.time };   // a value with its own time

Remembering a value between runs

Every run starts with empty outputs, and variables in the code do not survive from one run to the next. To remember something — a running total, a record, a smoothed value — keep it in a field, and map that field twice, with two different variable names:

IOJS variableFieldOptions
INliterswater/literstrg
INtotalwater/total—
OUTnewTotalwater/total—

The code reads the previous total from inputs.total and writes the new one to outputs.newTotal; the next run reads it back. The IN row of the remembered field must not have trg ticked — otherwise the script would start again from its own output. The mapping refuses that combination. See examples 9–11.

On the very first run the remembered field is still empty, so parseFloat() gives NaN — start from a sensible default, as the examples do. Values that arrive within a few milliseconds of each other may both read the same previous total; for sensors that report every second or slower this does not occur.

5. When a script runs

A script runs only when it is active and its mapping is complete (green icon).

Each output enters the platform like any other value, tagged with source SCRIPT: it is saved, shown on dashboards, sent via MQTT if pub is ticked, and can trigger rules, HTTP Requests and other scripts. When one script's output starts another script, the chain can be at most five scripts long; after that, no more scripts start. Values written by a rule never start a script. The full picture is in the Data Flow guide.

6. What the code can use, and the limits

Scripts run in an isolated JavaScript sandbox. Available: the whole standard language — Math, JSON, Date, arrays, strings, try/catch, functions — plus console.log(). Not available: network requests, files, timers such as setTimeout, and require or other modules. To fetch data from the internet, use HTTP Requests and let a script process the stored response.

LimitValueWhat happens
Run time50 ms per runThe run is stopped, nothing is written, and the script shows runtime error.
Memory32 MB per runThe same.
Script chain5 scriptsFurther scripts in the chain do not start.
Timer interval1 second or moreDefault 60 seconds.
Times are in UTC. new Date().getHours() returns the UTC hour, not your local hour. For actions at a local time of day, use a Scheduler, which follows the dashboard's time zone.

7. Testing and troubleshooting

In the code editor, the Input Simulation panel lists every IN variable with a box for a test value. Press Run Test: the code runs in your browser with those values, and Console Output shows your console.log() lines, the outputs the script would write, or the error it hit. A test writes nothing to your fields, so you can try values freely.

console.log() output is shown only in the test console. When the script runs live, it is not displayed anywhere — use the test panel to check your logic, and the Data Monitor to see what the live script actually writes.
The script does not run or writes nothing? Check, in this order:
  1. The icon is green. Amber means the mapping is incomplete (Section 3) — saving the mapping tells you exactly what is missing; grey means active status is off.
  2. At least one IN row has trg ticked, or the timer is on.
  3. only on change is not holding it back — the value really changed.
  4. trigger from includes the source of the value. Values written by a rule never start a script.
  5. The code does not stop early: an input that is empty or not a number makes the isNaN check return before anything is written.
  6. The OUT rows point to the right fields, and pub is ticked if a board must receive the value.
  7. No runtime error is shown under the script's name.

8. Examples

Each example shows its IO mapping and the complete code. The field names are only examples — map your own fields and keep the JS variable names, or change both. trg and pub in the Options column are the checkboxes of the mapping row.

1. Calibrate a raw ADC reading On event

A board sends the raw 12-bit ADC value (0–4095) of a temperature transmitter with a −50…150 °C range. The script turns it into degrees.

IOJS variableField (example)Options
INrawtank/temp_rawtrg
OUTcelsiustank/temperature—
// 0 … 4095 on the ADC  =  -50 … 150 °C on the transmitter
const raw = parseFloat(inputs.raw.value);
if (isNaN(raw)) return;                  // no value yet, or not a number

const celsius = raw * 200 / 4095 - 50;
outputs.celsius = Math.round(celsius * 10) / 10;   // one decimal

2. Thermostat with a dead band On event

Switches a heater around a setpoint that you change from the dashboard. Inside the dead band the script writes nothing, so the heater keeps its current state and does not chatter.

IOJS variableField (example)Options
INtemproom/temperaturetrg
INsetpointroom/setpointtrg
OUTheaterroom/heaterpub
const BAND = 0.5;                        // °C above and below the setpoint

const temp     = parseFloat(inputs.temp.value);
const setpoint = parseFloat(inputs.setpoint.value);
if (isNaN(temp) || isNaN(setpoint)) return;

if (temp < setpoint - BAND) {
    outputs.heater = 1;                  // too cold: heater on
} else if (temp > setpoint + BAND) {
    outputs.heater = 0;                  // warm enough: heater off
}
// Inside the band: no output at all, so the heater stays as it is.

3. Safety interlock On event

The pump may run only when it is requested, the emergency stop is released and the pressure is not too high. Any change on any of the three inputs re-evaluates the output at once.

IOJS variableField (example)Options
INrequestpump/requesttrg
INestopplant/estoptrg
INpressureHighplant/pressure_hightrg
OUTpumppump/runpub
const request      = parseFloat(inputs.request.value) === 1;
const estopPressed = parseFloat(inputs.estop.value) === 1;
const pressureHigh = parseFloat(inputs.pressureHigh.value) === 1;

// Run only if everything is safe. A missing input counts as "not requested".
outputs.pump = (request && !estopPressed && !pressureHigh) ? 1 : 0;

4. Compare two sensors On event

Two sensors measure the same thing. The script writes the difference and raises an alarm when they drift apart — a sign that one of them needs attention.

IOJS variableField (example)Options
INaboiler/temp_atrg
INbboiler/temp_btrg
OUTdiffboiler/temp_diff—
OUTalarmboiler/drift_alarm—
const LIMIT = 4.5;                       // allowed difference

const a = parseFloat(inputs.a.value);
const b = parseFloat(inputs.b.value);
if (isNaN(a) || isNaN(b)) return;

const diff = Math.abs(a - b);
outputs.diff  = Math.round(diff * 10) / 10;
outputs.alarm = diff > LIMIT ? 1 : 0;

5. Watchdog for a silent sensor Timer

Runs on a timer, every 60 seconds. If the sensor has not sent a value for 5 minutes, the status field turns to 0 — a Rule on that field can then send you a notification.

IOJS variableField (example)Options
INtempgarden/temperature—
OUTokgarden/sensor_ok—
const MAX_AGE_MIN = 5;

const hasValue = !isNaN(parseFloat(inputs.temp.value));
const ageMin   = (Date.now() - new Date(inputs.temp.time).getTime()) / 60000;

outputs.ok = (hasValue && ageMin < MAX_AGE_MIN) ? 1 : 0;

6. Read values from a JSON message On event

A gateway sends one JSON text with several readings. The script writes each reading to its own field. (For this job without code, see the JSON Splitter under Tools.)

IOJS variableField (example)Options
INpayloadgateway/datatrg
OUTtempgateway/temperature—
OUThumgateway/humidity—
// Example message: {"temperature": 22.4, "humidity": 61}
let data;
try {
    data = JSON.parse(inputs.payload.value);
} catch (e) {
    console.log('Not valid JSON:', inputs.payload.value);
    return;
}

if (typeof data.temperature === 'number') outputs.temp = data.temperature;
if (typeof data.humidity === 'number')    outputs.hum  = data.humidity;

7. Keep the measurement time On event

Calculates power from voltage and current. The result is stored with the time of the current reading, not the time the script ran — useful when a board sends buffered readings with their own timestamps.

IOJS variableField (example)Options
INvoltsmeter/voltage—
INampsmeter/currenttrg
OUTwattsmeter/power—
const volts = parseFloat(inputs.volts.value);
const amps  = parseFloat(inputs.amps.value);
if (isNaN(volts) || isNaN(amps)) return;

outputs.watts = {
    value: Math.round(volts * amps * 10) / 10,
    time:  inputs.amps.time               // same timestamp as the current reading
};

8. Average of several sensors Timer

Runs on a timer and writes the average of three room sensors. Sensors without a value are left out, so one dead sensor does not spoil the result.

IOJS variableField (example)Options
INt1house/kitchen_temp—
INt2house/living_temp—
INt3house/bedroom_temp—
OUTaveragehouse/average_temp—
const values = [inputs.t1, inputs.t2, inputs.t3]
    .map(input => parseFloat(input.value))
    .filter(v => !isNaN(v));             // skip sensors without a value

if (values.length === 0) return;

const sum = values.reduce((total, v) => total + v, 0);
outputs.average = Math.round(sum / values.length * 10) / 10;

Examples that remember a value

These three keep their result in a field that is mapped both as IN and as OUT — see Remembering a value between runs.

9. Running total (water meter) On event

A water meter sends the litres used since its previous message. The script adds them to a running total. The total field is mapped twice: as IN to read the current total, and as OUT to write the new one.

IOJS variableField (example)Options
INliterswater/literstrg
INtotalwater/total—
OUTnewTotalwater/total—
const liters = parseFloat(inputs.liters.value);
if (isNaN(liters)) return;

let total = parseFloat(inputs.total.value);
if (isNaN(total)) total = 0;             // first run: the total field is still empty

outputs.newTotal = Math.round((total + liters) * 100) / 100;

10. Lowest temperature On event

Keeps the lowest temperature ever measured. The script writes only when a new record is set. To start over, write a high value such as 999 to the record field from the dashboard.

IOJS variableField (example)Options
INtempgarden/temperaturetrg
INlowgarden/temp_lowest—
OUTnewLowgarden/temp_lowest—
const temp = parseFloat(inputs.temp.value);
if (isNaN(temp)) return;

const low = parseFloat(inputs.low.value);
if (isNaN(low) || temp < low) {
    outputs.newLow = temp;               // a new record (or the very first value)
}

11. Smooth a noisy reading On event

A level sensor jumps up and down. Each new reading moves the smoothed value only part of the way towards it (an exponential moving average), so single spikes barely show.

IOJS variableField (example)Options
INrawtank/level_rawtrg
INsmoothtank/level—
OUTnewSmoothtank/level—
const K = 0.2;                           // 0…1: smaller = smoother but slower

const raw = parseFloat(inputs.raw.value);
if (isNaN(raw)) return;

const previous = parseFloat(inputs.smooth.value);
const next = isNaN(previous) ? raw : previous + K * (raw - previous);

outputs.newSmooth = Math.round(next * 100) / 100;

Virtuino Cloud — Scripts Guide