New: the AI firewall watches every module and explains each incident in plain words. See how it works

Compute scripts

Reshape a card's data with a small sandboxed JavaScript function – the contract, what it may and may not do, limits, error handling and worked examples.

A compute script (shown as Transform in the card’s settings) is a small JavaScript function that derives what a card draws from the data it loaded. Use it when no source gives exactly the shape you need: convert units, pair two series into a route, count rows, pick the worst status. A script only computes. It cannot set a parameter, run a command, open a page or reach the network.

Add a script

  1. Customise the board and open the card’s settings.
  2. On the Data tab, open Advanced: compute the values with a script.
  3. Tick Run this script and write the Script.
  4. Select Run now to run your draft on the card’s current data. The Result lists what came back per slot, for example “series: table, 12 row(s)” and “ran in 3 ms”.
  5. Set the Time budget (ms) and When a run fails.
  6. Select Save.

Remove the script deletes it. The script is copied with the card, travels in board files, and stays on the card when you save it as a widget.

The contract

The script is an ES module whose default export is a synchronous function:

export default function transform({ slots, params, window, now }) {
  // slots:  the bound data by slot key, as the source returned it (shape table | scalar | series | record)
  // params: the effective board parameters of the card's page (read-only)
  // window: { from, to, interval? } (ms since the epoch) when the card's data ran with a window
  // now:    ms since the epoch, the host's clock (a script never reads Date)
  return { slots: { /* the slots you replace */ } };
}

It returns { slots }. Each returned slot replaces the loaded one; slots you do not return stay as loaded. An empty result draws the data as loaded.

ShapeForm
Scalar{ shape: 'scalar', value: 21.5, meta: { unit: '°C', decimals: 1 } }
Series{ shape: 'series', series: [{ name: 'temperature', points: [{ t: '2026-11-09T10:00:00Z', v: 21.5 }] }] }
Table{ shape: 'table', columns: [{ key: 'id' }, { key: 'value', type: 'number' }], rows: [{ id: 'p1', value: 3 }] }
Record{ shape: 'record', record: { level: 2.4, flow: 18 } }

A process diagram or a 3D model card may also return scene: things, per-object overrides, extra connections and zones, the view, and problems. It only changes the drawing.

Where it runs

The script runs in your browser, in a hidden frame with no access to Hermetry, the network or your session, inside its own worker. It runs again whenever the card’s data changes (a live update included) or a parameter changes. One run per card is in flight at a time; the latest input wins, at most 20 runs a second. A card that is not shown does not run. Display screens run scripts the same way.

Limits

LimitValue
Script length65,536 characters
Time budget10 to 500 ms per run, default 50. Past it the run is stopped.
Returned slots16
Rows per table5,000
Columns64
One text value10 KiB
Whole result2 MiB
Things in a scene500 on a process diagram, 2,000 on a 3D model

When a run fails

When a run failsThe card
Keep the previous resultKeeps the last good output and shows a Transform failed badge
Show the card emptyShows the error in place of the data

Messages: “The script ran past its time budget: …”, “The script could not be loaded: …”, “Transform failed: …”.

Examples

Celsius to Fahrenheit on a stat card (slot value):

export default function transform({ slots }) {
  const c = slots.value && typeof slots.value.value === 'number' ? slots.value.value : null;
  if (c === null) return {};
  return { slots: { value: { shape: 'scalar', value: c * 9 / 5 + 32, meta: { unit: '°F', decimals: 1 } } } };
}

Count the open alarms of a parameter’s devices (a stat card whose value slot is bound to Alarms (live), a table):

export default function transform({ slots }) {
  const rows = slots.value && Array.isArray(slots.value.rows) ? slots.value.rows : [];
  return { slots: { value: { shape: 'scalar', value: rows.length, meta: { decimals: 0 } } } };
}

A route from latitude and longitude series (a map whose routes slot is bound to Device readings over time with the keys lat,lng):

export default function transform({ slots }) {
  const series = slots.routes && Array.isArray(slots.routes.series) ? slots.routes.series : [];
  const named = (k) => series.find((s) => s && s.name === k) || { points: [] };
  const lng = new Map(named('lng').points.map((p) => [p.t, p.v]));
  const track = named('lat').points
    .filter((p) => lng.has(p.t))
    .map((p) => [p.v, lng.get(p.t), Date.parse(p.t)])
    .sort((a, b) => a[2] - b[2]);
  return { slots: { routes: { shape: 'table',
    columns: [{ key: 'name' }, { key: 'positions' }],
    rows: track.length >= 2 ? [{ id: 'route', name: 'Route', positions: track.slice(-256) }] : [] } } };
}

A list inside a cell holds at most 256 items, so keep a route to 256 points. See Map cards.

Good practice

  • Return only the slots you change.
  • Guard against missing data: a slot can be empty while the device is silent.
  • Keep scripts small and fast; a long loop over a year of raw readings will hit the time budget. Prefer a source with an aggregate.
  • Use now, never Date.now(), for anything time-based.