Appearance
Script Sandbox, Limits & Errors
Last updated
Each run executes in a separate, locked-down script host service (scada-ink-scripts), not in the API process. One throwaway process handles one run and is discarded afterwards. The script still sees the same JavaScript environment and the same Scada.Ink API — it simply runs behind a wall: it cannot read the appliance's files, the configuration database, the licence or any OS setting, and nothing it does can reach the API process. When the script calls an API function (tags.read, db.query, users.add …), that call is handled by the API; the results and errors are exactly as before.
Nothing carries over from one run to the next: a variable set in one run is gone in the next. Keep state you need between runs in a local tag or a Data Table.
How your code is wrapped
Your code is placed inside an async function, after the code of every script library:
javascript
(async () => {
// every library, as: const <name> = (function () { const exports = {}; ... return exports; })();
// your script
})()So you can:
- use
awaitat the top level; - use
returnat the top level to stop early (the returned value is discarded — only the log and the error are stored); - use every library by its name as a global, e.g.
utils.clamp(v, 0, 100).
And you cannot declare a top-level variable with the same name as a library: const utils = 1; fails with SyntaxError: Identifier 'utils' has already been declared.
What is available
- The Scada.Ink API objects:
tags,devices,db,namedQueries,alarms,auth,users,system,email,history,http,workflows,ladder,messages,expression,workOrders, pluslog,console,sleep,setTimeout,setInterval,clearTimeout,clearIntervalandcontext. All documented in Scripting API Reference. - Standard JavaScript:
Math,JSON,Date,Number,String,Boolean,Array,Object,Promise,Map,Set,RegExp,parseInt,parseFloat,isNaN,isFinite, and the other built-ins of the language such asErrorandencodeURIComponent.
What is not available
| Missing | Use instead |
|---|---|
fetch, XMLHttpRequest | http.get/post/put/delete |
require, import, npm packages | Script Libraries |
process, Buffer, URL, file system access | Not available. system.info() gives version and uptime. |
| SMS, recipes, OEE APIs | A workflow (call it with workflows.run()) or the REST API through http |
Using one of these fails with, for example, ReferenceError: fetch is not defined. setTimeout and setInterval work in a saved script but not in a library's Run Test.
Time limits
| Limit | Value | Detail |
|---|---|---|
| Continuous execution | 30 s | A tight loop such as while (true) {} is stopped with Script execution timed out after 30000ms. |
| Total run time | 30 s wall-clock | Counts time inside await too (HTTP, sleep, workflows.run), so several waits in the same run can add up to Script timed out after 30s (this includes time spent waiting on network calls or sleep(), not just running code) even though no single wait was long. |
sleep(ms) | 10 s per call | Larger values are cut to 10 000 ms. Call it repeatedly if you really need longer — up to the 30 s total. |
http.* | 10 s per request | Then the call throws a timeout error. |
| Timers | 20 pending per script | setInterval at least 100 ms. Each timer callback may run 5 s of continuous code. Timers are not bound by the 30 s run limit — see Timers. |
db.query limit | 1000 | Only applied when you pass limit. |
| Library Run Test | 15 s | Continuous execution. |
| Workflow ↔ script nesting | 5 levels | A workflow → script → workflow chain stops at depth 5. |
Memory limit
Each run has 128 MB of memory. A run that goes over — for example, building one enormous array or string — is stopped and marked failed with Script exceeded the 128 MB memory limit and was stopped. The appliance itself is unaffected: the run is contained in its own process. Keep per-run work small; read in batches and process fewer rows per run.
Speed of API calls, and batching
Because each run is now in its own process, every API call makes a short round trip to the API — on the order of a millisecond each, rather than being effectively free. A script that loops over thousands of individual tags.read or single-row db calls is noticeably slower than before. Prefer the batch calls, which do the work in one round trip:
tags.readAll(deviceId)/tags.readAllQualified(deviceId)instead of manytags.read;tags.readBatch/tags.writeBatchfor a known list of tags;- a named query instead of many small
db.querycalls.
Timers and the 30 s limit
Timers you start with setTimeout / setInterval keep running after the run's own code has finished, in that run's process, until they fire, are cleared, or the script is stopped, disabled or re-saved. One case changed: if a run's own code hits the 30 s wall clock, the run is stopped and any timers it had scheduled are stopped with it. (A run that finishes normally and leaves timers pending is unaffected.)
Errors
When a script throws (or a promise it awaits rejects):
- The run is marked failed.
- The error is stored and added to the log as
ERROR: Name: message (line N, col M). - Every browser connected to Scada.Ink shows a warning toast titled Script Failed with
script name: error. _STATUS/Script/<script name>/Failedon the Local Tags device goestrue, with the message in…/LastError. It goes back tofalseafter the next run that succeeds.
A toast is seen only by someone with a screen open. For a script that runs unattended — a nightly export, an hourly report — add an alarm rule on _STATUS/Script/<script name>/Failed (device Local Tags, Equal to 1) and attach it to a pipeline that emails or texts you. See System Status Tags.
console.error() does not fail the run; it only adds a line starting ERROR: . console.warn() adds WARN: . To fail the run on purpose, throw new Error('why').
Line numbers match the editor's. When the error happened inside a library your script called, the message names it instead: TypeError: … (library utils, line 12).
Under the error, the log lists the stack — the chain of calls that led to it, innermost first, one line per function in your script or a library, on your own line numbers:
text
ERROR: TypeError: Cannot read properties of null (reading 'x') (line 4, col 15)
at readWeight (line 4, col 15)
at checkBatch (line 9, col 3)
at (line 12, col 1)
at weigh.tare (library weigh, line 7, col 5)Frames inside Scada.Ink itself are left out, and at most 10 are shown. In the editor's Log tab, a failed run whose error names a line has a Go to line N button that puts the cursor on it.
Failures inside fire-and-forget calls do not fail the run: tags.writeDevice and workflows.trigger report their errors as ERROR: lines in the log only if the run is still going when they fail.
Security model
- A script reaches the appliance only through the Scada.Ink API functions listed here — never the files on disk, the configuration database, the licence, the operating system, or the API process. It runs in a separate, locked-down service, so even code that tries to break out of the sandbox reaches only its own throwaway process.
- A script runs with full server access to those functions, regardless of who triggered it. It does not run as the user who pressed the button.
users.*andsystem.backup()work only if the code was last saved by an administrator. When an editor saves a change to the code, those calls start failing withusers.add is restricted: this script must be saved by an administrator to use it.- Named queries called from a script run with the authority of the last person who saved the code: admin if that was an administrator, otherwise editor.
httpis gated by network switches (below). It can reach public and plant-network addresses when allowed, but never a loopback service other than the appliance's own API port, and never the appliance, licensing, update or security routes even over that port.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Trigger never fires | Script is disabled | Switch the toggle to Enabled, in the editor or on the Scripts list. |
| Tag Change never fires | The script is Disabled (new scripts start disabled), or the wrong tag is selected | Turn the script on with the toggle in the toolbar; check the Tag field shows the right device and tag. |
| Tag Threshold runs constantly | It runs on every update while the condition is true | Tick Only when it crosses (with a hysteresis for a noisy value), see Script Triggers & Context. |
| Edits have no effect on Run Now | Run Now runs the saved code | Save first (Ctrl+S). |
ReferenceError: X is not defined | A Node/browser global that the sandbox lacks, or a library name typo | See "What is not available". |
SyntaxError: Identifier 'x' has already been declared | A top-level variable has the same name as a library | Rename the variable. |
A script fails with … is not a function on a library call | That library has a syntax error, or threw while loading — it's loaded as empty rather than failing every script | Fix the library on the Script Libraries page, using Run Test. |
… is restricted: this script must be saved by an administrator to use it. | users.* / system.backup() in a script last saved by a non-admin | An administrator opens the script and saves it. |
Write access is not enabled for device N. Enable it in device settings. | tags.writeDevice on a device without writes enabled | Enable writes on the device. |
Device not found: X | Name passed to writeDevice, history.* doesn't match a device | Check spelling in devices.list(). |
Table not found: X | db.* table name doesn't match a Data Table's name | Use the name shown on the Data Tables page. |
HTTP 404 from GET https://… (or another status) | The server answered with an error status | Check the URL and credentials. Catch the error and read err.status / err.body, or use http.request to handle the status yourself. |
Local device not available | The built-in local device is missing | Restart the service; it is recreated at start. |
Script execution timed out after 30000ms | More than 30 s of continuous computation | Split the work, process fewer rows per run. |
Script timed out after 30s (this includes time spent waiting on network calls or sleep(), not just running code) | The run's total wall-clock time, sync work plus every await, passed 30 s | Fewer or shorter sleep()/http.* calls per run, or split the work across more runs. |
Script exceeded the 128 MB memory limit and was stopped. | The run allocated more than 128 MB | Process fewer rows per run; read in batches; don't build one giant array or string. |
Loopback addresses are refused for scripts, except the appliance's own API port. | An http call to localhost/127.0.0.1 on a port that isn't the API | Use the Scada.Ink API (tags, Data Tables, named queries) to reach local services like InfluxDB or the drivers; only the appliance's own API port is allowed over loopback. |
This script may not reach the internet … / … the plant network … | An http call blocked by a network switch | An administrator turns on Internet or Plant network for the script, and the master switch in Settings. |
Script network access is turned off for this appliance. | The appliance-wide master switch is off | An administrator turns on Allow scripts network access in Settings. |
Scripts are not running: the script host service (scada-ink-scripts) is not installed … | The appliance was updated in-app but the Installer hasn't been re-run | Re-run the Installer from the System page (see the Installer update card). Scripts never fall back to running in the API process. |
Workflow not found: X / Ladder task not found: X | Name or id doesn't exist | Check workflows.list() / ladder.list(). |