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
- Customise the board and open the card’s settings.
- On the Data tab, open Advanced: compute the values with a script.
- Tick Run this script and write the Script.
- 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”.
- Set the Time budget (ms) and When a run fails.
- 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.
| Shape | Form |
|---|---|
| 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
| Limit | Value |
|---|---|
| Script length | 65,536 characters |
| Time budget | 10 to 500 ms per run, default 50. Past it the run is stopped. |
| Returned slots | 16 |
| Rows per table | 5,000 |
| Columns | 64 |
| One text value | 10 KiB |
| Whole result | 2 MiB |
| Things in a scene | 500 on a process diagram, 2,000 on a 3D model |
When a run fails
| When a run fails | The card |
|---|---|
| Keep the previous result | Keeps the last good output and shows a Transform failed badge |
| Show the card empty | Shows 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, neverDate.now(), for anything time-based.

