Scripting
Scadex runs small JavaScript scripts in a sandbox beside the gateway: to compute a tag, to react when someone uses a screen, to do scheduled work, and to try things out. This page is for engineers who write any of them. It covers what is common to all scripts; the pages linked at the end cover each kind.
Where scripts run
| Kind | Runs when | Page |
|---|---|---|
| Expression tag | One of the tags it reads changes | Expression tags |
| Screen event script | Someone clicks a component or changes a value on a screen | Screen scripts |
| Cron job script | The schedule fires, someone runs the job by hand, or a missed run is caught up | Cron scripts |
| Script console | An administrator presses Run on the Script console page | Script console |
| Module script | A module calls IModuleContext.Scripts.RunAsync (module contract 1.3) | Writing a module |
The cron module is built on module scripts, so a cron script and a module script have the same limits and run as the same identity.
The language and the sandbox
A script is a JavaScript function body: it uses return to hand back a value. The gateway wraps it in a strict-mode function, so "use strict" rules apply.
Scripts run in Jint, outside the gateway process, in a small pool of Scadex.ScriptWorker processes that talk to the gateway over stdin and stdout. Each run gets a fresh engine. Inside it:
- There is no access to the .NET runtime (no CLR objects reach a script), and no
evalorFunction. ShadowRealm,Atomics,SharedArrayBufferandWebAssemblyare removed.- Calls nest at most 64 deep, arrays hold at most 100,000 elements, and a source is at most 64 KB.
- A script reads at most 256 tags, writes at most 64, and calls the gateway through
system.*at most 100 times. Eachsystem.*call sends at most 256 KB of arguments. - A tag path is at most 512 characters.
A worker's environment holds none of the gateway's settings or secrets. It is not a stronger security boundary than Jint itself: the worker has the gateway's user and file access.
Limits per kind
Time is CPU time, so a host too busy to run the script, or a tag read that is slow to answer, does not count against it.
| Kind | Statements | Memory | CPU time |
|---|---|---|---|
| Expression tag | 50,000 | 16 MiB | 250 ms |
| Screen event script, script console | 500,000 | 32 MiB | 2 s |
| Module script, cron job | 2,000,000 | 32 MiB | 5 s |
Other limits are the same for every kind:
- A script's tag reads have their own limit of 5 s in all.
- The pool keeps 2 workers ready from startup and grows to at most 4 (never more than the machine's cores), each using about 50 to 60 MiB. A worker's heap is capped at 256 MiB.
- A script stuck where Jint cannot check its limits, such as inside a built-in, is killed one second after its limit. One that crashes its worker (a stack overflow, running out of memory) ends only that worker; the gateway starts another.
- After a kill or a crash, that exact script is refused with the same error for 5 seconds, doubling each time it happens again, up to 5 minutes. A run that completes clears the penalty.
- When no worker is free within 8 s, the script fails with "The gateway is too busy to run this script."
- Module scripts take turns, one at a time across the gateway, so they never hold the workers that screens and expression tags rely on. A module script waits at most 30 s for its turn.
Who a script runs as
A script never has more authority than the person or job that runs it. system.* calls that change something check the caller's role (see Users and roles).
| Kind | Identity | Role | Changes |
|---|---|---|---|
| Script console | The administrator who presses Run | Their role (admin) | Dry run by default: writes and changing calls are reported, not made; database statements run in a transaction that is rolled back. Run for real applies them |
| Screen event script | The signed-in user of the /sync session | Their role as it is at that moment | Tag writes need the operator role. Session- and user-scoped memory tags are the session's own |
| Cron job | cron:<job name> for a scheduled or caught-up run; the user who pressed Run now for a manual run | Engineer | Writes applied after the script, audited under that identity |
| Module script | The PrincipalId the module names | Engineer | As for a cron job; DryRun reports instead |
| Expression tag | None | None | Cannot write and cannot call the gateway: only system.tag.read, system.date and system.util |
What every script can reach
| Global | What it is |
|---|---|
tag(path) | The tag's live value, in engineering units. A path is a string such as "Plant.Tank1.Level" |
tags | An object of the tags the gateway read before the script started, by path |
writeTag(path, value) | Asks for a write. Writes are applied in order once the script has finished; a script that fails writes nothing. Not in expression tags |
event | What triggered the script. Its fields depend on the kind; see each page |
val | A screen event's value (event.value). undefined elsewhere |
console.log, .info, .warn, .error, .debug | Logging. Kept in the console and in cron run records (at most 200 lines and 64 KB per run, 2,000 characters per line); accepted and dropped elsewhere |
Math, JSON, Date, strings, arrays | Standard JavaScript |
system.* | The gateway's API by area: tag, alarm, db, gateway, user, date, util. See The system API |
Values cross as plain JSON. A tag value reaches a script as a boolean, number, string or object; a DateTime arrives as an ISO 8601 string and bytes as base64. A whole number a script returns is stored as an integer when it fits in 53 bits, any other number as a double, and an object or array as a JSON value.
Type-checked editors
Every script editor in the dashboard checks the script as TypeScript would, against one contract: apps/dashboard/script-types/scadex-script.d.ts, plus a small file per context (expression.d.ts, screen.d.ts, cron.d.ts, console.d.ts) that declares which parts of system that context may use. A wrong name, a missing argument or a typo in a tag path shows as you type; hover a name for what it does and press Ctrl+Space for what is there.
Tag paths are typed from the gateway's tag list: the editor generates interface TagValues { "Plant.Tank1.Level": number; ... } for every tag that holds a value (folders, tag types and anything under @types are left out). Boolean tags are boolean, numeric tags number, string and date-time tags string, other kinds unknown. A path the editor cannot know, such as a relative path, one with a {param} or one built at run time, is typed any rather than flagged, so a working script is never shown as broken.
Errors and where they surface
A script fails when it throws, when it hits a limit, or when a system.* call is refused. The error text names the cause, for example The script exceeded the 50000 statement limit. or system.alarm.acknowledge: Acknowledging alarms needs the operator role, and this script runs as viewer. Error messages are cut at 500 characters.
| Kind | What happens |
|---|---|
| Expression tag | The tag goes to Bad quality with the message in qualityDetail, keeping its last value |
| Screen event script | The /sync reply is screen.event {ok: false, errors}; no write is made and no directive returned. The dashboard shows the error |
| Cron job | The run record has outcome failed and the error; the job's failure tag, if it has one, is set to true |
| Script console | The Result pane shows Failed and the error under What went wrong |
| Module script | ModuleScriptResult.Ok is false and Error holds the text |
Pages in this section
- Expression tags: compute a tag from other tags.
- Screen scripts: react to clicks and value changes, write tags, show toasts, navigate.
- Cron scripts: scheduled work with the cron module.
- Script console: try a script as yourself.
- The system API:
system.tag,system.alarm,system.db,system.gateway,system.user,system.dateandsystem.util.