Actions
The action editor: identity and location, the script block, what a script may read and write, every validation message, the Run verb and its journal, who fired the last run, what runs the action, Enable and Disable, Copy and Delete, and the run limits.
An action is a JavaScript statement block that reads the address space and writes memory variables and tags. Actions are the write path of Logic: they run on demand (Run), on a trigger, from a dashboard Button wired to them, or at the Start or the Completion of a Process run, through a Logic action command. Select an action under the Actions section of the Logic column, or press Open on its row in the table of its folder or of its section root, to open its editor (an action made with New… > New action is not opened but marked in that table, see Creating and copying); reading needs View logic, editing Configure logic, running Operate logic.
The header shows the action mark, the name, the line "A script you run on demand, or that a trigger runs for you, writing variables and tags." and a Disabled pill while the action is off. The form column holds IDENTITY, SCRIPT and Used by; the rail holds LAST RUN.
Identity
| Field | What it is | Values / default | Effect |
|---|---|---|---|
| Name | The display name and the label a run is journaled under. | Free text; a new action is ActionN; a blank name saves as Action. |
Autosaves per keystroke. Actions are referenced by identity, so renaming rewrites nothing and breaks nothing. |
| Location line | Where the action is filed, in words: Actions / Startup / Walk setpoint. |
Read only. | An action has no address; the line is a place, never something to type into an expression. Change it with Move to in the tree. |
| Description (optional) | Free text. | Empty by default. | None. |
Script
The SCRIPT card is the script editor with the hint "JavaScript statements (let, if, for, functions). Write memory variables by assignment (Variables.Setpoint = 21.5;) and tags with Connector.Driver.Device.Tag.Write(value). Reads are not quality-gated: check .State() and guard gaps with ?? before arithmetic." The script autosaves per keystroke and is validated every half second; the message appears under the hint. Validation never executes the script.
A script is ordinary strict-mode JavaScript (locals, if, loops, functions, Math, Date, JSON) over the same address space and the same read surface as expressions, with two differences. Reads are not quality-gated: a gap reads as null, so check .State() and guard arithmetic with ??; handling a bad reading is what actions are for. And a script may read expression variables (their published value); only an expression may not reference them.
A live point (a tag, a variable, an alarm, a Process channel) is the point, not its value, so read its value through a member: if (Variables.Start.ValueToBool()), Variables.Level.ValueToReal() > 80, if (Alarms.High.IsActive). Written bare in the test of if, while, do, for or ?:, as an operand of !, && or ||, in a comparison or in arithmetic (+= and the other compound assignments included), it would always count as true or never read its value: if (Variables.Start) { … } would run its block on every run whatever the variable holds. Validation refuses it and names the member to read instead (What validation refuses). A point handed to a function, and any member of a point, stays as written. The same holds for a switch on a bare point or a case that names one (read .Value), the left side of ?? (read .Value) and a placeholder ${…} of a template literal (read .ValueToString()). A compound assignment to a variable (Variables.Total += 1, -=, ||= and the rest) reads the variable as the point before it writes, so it is refused with the plain assignment that works: Variables.Total = Variables.Total.ValueToReal() + 1.
What an action may write
Exactly two constructs mutate the plant, and both go through the journaled write funnel:
| Write | Form | Target |
|---|---|---|
| Memory assignment | Variables.Setpoint = 21.5; or Variables.Forno.Setpoint = 21.5; |
A memory variable, folder levels included. The value is coerced to the variable's output type and marked for retention when the variable is retentive. It applies at once, so the next line of the same script reads what it just wrote. |
| Tag write | Connector.Simulated.Pump.Cmd.Write(true); or …Cmd.WriteAndWait(true); |
A live tag. The value is an engineering value: it travels backwards through the tag's conversion pipeline to the raw value the device takes, and the funnel hands it to that device's own delivery queue, where the values owed to that device are sent one at a time in the order they arrived. |
A tag carries two write verbs, and the script picks by whether it needs the answer:
| Verb | Arguments | Left out | Returns | Waits for the equipment |
|---|---|---|---|---|
Write(value) |
The value. | Nothing to leave out. | Nothing. | No. The value is handed to the device's queue and the next line runs. |
WriteAndWait(value, timeoutMs, continueOnFailure) |
The value, how many milliseconds to wait for it, and whether to go on when it fails. | timeoutMs: the driver's own waiting time is used instead. continueOnFailure: true. |
true only when the equipment took the value; false for anything else, a superseded value included. |
Yes, until the receipt arrives or the waiting time runs out. |
The wait happens on the actions lane, so a slow device holds back only the actions behind this one, and it never withdraws the write: a value that is not confirmed in time is still travelling, and its receipt is journaled when it lands. That is what the answer says, in as many words: "'Cmd' was not confirmed within 2000 ms; it is still on its way to the equipment." With continueOnFailure left out or true the verb answers false and the script carries on; with false the run ends at that line, naming the target and the reason.
No write ever aborts a script by itself. A write refused before anything is sent and a write the equipment refuses or never answers both fail alone: the rest of the script runs, and the run finishes as a run that succeeded with failed writes. Nothing is rolled back either, so what applied before a failed write stays applied, and the journal shows each write as old to new under the source Action. The reasons a refusal is read in:
- "Write to 'X' rejected: the value is not finite." (NaN or infinity)
- "'Tag' is not materialized in the connector." (a disabled device, or a tag the Connector does not carry)
- "'value' could not be converted for 'Tag'."
- "'X' is write-protected by '
<holder>'." (a recipe run or an interlock holds the source) - "'Cmd' was not written: the device is not connected." (the device is already known to be down, so nothing is queued for it)
- "Write to 'Tag' failed: the Connector is not running."
- "Variable 'X' was not found." and "'Variables.X' is an expression variable: it is computed, never written."
- Whatever the equipment or its driver answered, in its own words, when the refusal came from the wire.
A value that a newer write to the same tag overtook in the device's queue is not a failure: "'Cmd' was replaced by a newer value before it was sent." is exactly what happened, the newer value is the one the equipment is told, and nothing is reported anywhere except to a WriteAndWait, which answers false because this value is not the one that left.
What does end a run: an error of the script itself (a syntax error, an address that names nothing, an exception), a WriteAndWait(…, false) whose write failed, and the run limits under Run.
Which failures the run itself can report follows from who waited. A Write that is refused before anything is sent is refused while the script is still running, so the run counts it and the feed says so; a Write the equipment refuses afterwards is answered long after the run has ended, so it is the Events console under the action's name that carries it, and never the run. A WriteAndWait stayed for the answer, so it counts whichever way the answer went.
What validation refuses
| Message | Cause |
|---|---|
| "A script is required." | The editor is empty. |
| "Script error: …" | The text does not parse. |
| "Addresses must be static: 'Root' is accessed with a computed segment." | Variables[name] or any non-literal segment. |
| "Incomplete address '…'." | A reference too short to name a point. |
| "'X' was not found." | A read, or an assignment target, that resolves to nothing. |
| "'X' is a value: read it without .Value." | A live point's member (.Value, .ValueToBool(), .State(), …) read on a point that enters the script as its value: System.StartTime.Value, or Process.Oven_soak.Oven_1.Procedure.Running.ValueToBool(). The sentence names the member that was written. |
| "'X' is the point itself, not its value: as a condition it always counts as true. Read its value with X.ValueToBool()." | A live point written bare where it is tested for truth: the test of if, while, do, for or ?:, or an operand of !, && or \|\| (if (Variables.Start)). On an alarm the sentence names .IsActive. |
| "'X' is the point itself, not its value: a comparison or a calculation never reads the value from it. Read its value with X.ValueToReal()." | A live point written bare in a comparison or in arithmetic (Variables.Level > 80, total += Variables.Level). For ==, !=, === and !== the sentence names .Value, and on an alarm .IsActive. A switch on a bare point, or a case that names one, gets the .Value sentence. |
| "'X' is the point itself, not its value: it is never empty, so ?? always keeps it. Read its value with X.Value." | A live point written bare on the left of ?? (Variables.Level ?? 0). On an alarm the sentence names .IsActive. |
| "'X' is the point itself, not its value: written into a text it never shows its value. Read its value with X.ValueToString()." | A live point written bare in a placeholder ${…} of a template literal. On an alarm the sentence names .IsActive. |
| "'+=' reads X as the point itself, not its value, before it writes. Use X = X.ValueToReal() + 1." | A compound assignment to a variable: +=, -=, *=, /=, %=, **=, the bitwise ones, &&=, \|\|= and ??=. The sentence names the operator written and the plain assignment that does what it reads, with the right side as written: .ValueToReal() for arithmetic, .ValueToInteger() for the bitwise ones, .ValueToBool() for &&= and \|\|=, .Value for ??=. |
| "Assign to the variable itself: Variables.Name = value." | The left side of = names a member, or goes deeper than a variable's address can. |
| "'X' is an expression variable: it is computed, never written." | Assigning to an expression variable. |
| "'Connector' points are read-only. Write tags with Connector.Driver.Device.Tag.Write(value)." | Assigning to any root other than Variables (Process has its own message). |
| "Process points are read-only in Actions; run commands and staging writes are not supported by scripts." | Assigning to, or calling Write on, a Process point. |
| "Process run commands cannot be called from Actions; command points expose read-only readiness values." | Calling Process.<Model>.<Unit>.Commands.Start() or any other command point; a Commands point only reads whether the command could execute now. |
| "The ++ and -- operators are not supported on points. Use Variables.Name = Variables.Name.ValueToReal() + 1." | Variables.N++. |
| "Write(value) is the tag write: Connector.Driver.Device.Tag.Write(value)." | Write called on something that is not a tag address. The sentence names the verb that was used, so WriteAndWait gets the same one with its own name in both places. |
| "WriteAndWait's waiting time must be a number of milliseconds: WriteAndWait(value, 2000)." | The second argument is written out and is not a number. |
| "WriteAndWait's continue-on-failure must be true or false: WriteAndWait(value, 2000, false)." | The third argument is written out and is not true or false. |
| "WriteAndWait takes at most three arguments: the value, the waiting time in milliseconds and whether to continue on failure." | A fourth argument. |
The three WriteAndWait messages judge what the text itself says. An argument the editor cannot settle by reading it (a variable, a computed expression) is judged when the call is actually made, and the same sentence then ends the run at that line.
JavaScript locals (let x = 1; x = x + 1;) are untouched by these rules.
Run
Run is the leading verb of the bar over an open action; it is drawn only for a role with Operate logic and greyed with "Runtime stopped" while the station is read-only (the engineering lock does not grey it: running is operating, not configuring). When the role also holds Configure logic, the draft in the editor is saved first and the run executes the saved definition; without it, the run executes the durable definition (the fields are read-only, so the two never differ).
Every run executes on the actions lane with a fresh script engine, so no state leaks between runs and two scripts never run at once: they run one at a time, in the order they were asked for. The lane is not the 100 ms sweep. Expressions, alarms, acknowledgements and writes arriving from elsewhere keep going while a script runs, so a sweep can land in the middle of one and two reads in the same script may see different cycles; in exchange, an action that stops to wait for a device delays only the actions queued behind it and nothing else in the station.
A run is aborted past 500 ms ("The script was aborted: it ran past the 500 ms time limit (infinite loop?)."), 100,000 statements or 4 MB of memory, each with a message naming the limit. The 500 ms measures the script running: the count is held for as long as a WriteAndWait is waiting on a device and picked up again afterwards, so a correct script is never blamed for slow equipment. A disabled action fails without running ("The action is disabled."); a stopped runtime answers "The logic runtime is not running."
The feed reports "Run OK in 12 ms." when every write got through, "Run OK in 12 ms, 1 write failed: Cmd: 'Cmd' was not written: the device is not connected." when the script itself finished and a write did not (with "Run OK in 12 ms, 3 writes failed, the first: Cmd: 'Cmd' was not written: the device is not connected." from the second failure on), or "Run failed: <reason>" when the run itself ended badly.
The failed run reaches the Events console, under the action's name together with what invoked it: Manual for the verb, Dashboard for a Button, MCP for the agent, the trigger's name for a scheduled run, and Process · <unit> · <action> for a Process run. It is journaled as a warning the first time, summarized while it keeps failing with the same cause, and noted once more when it recovers, and that recovery line is the one entry a successful run ever writes there. A refused write reaches the console the same way and under the same name, and for the same reason it is the action that is named and not the run: the equipment's answer can arrive seconds after the run has ended, and there is no run left to fail. A run that simply succeeds, and every value it writes, is written only to the detailed capture: a script writing on every sweep produces values by the thousand, and a console filled with them is a console nobody can read the station's news in.
The LAST RUN rail shows when the action last ran (or "Never run."), "Duration: N ms", who fired it (below), the last error in a caution note, the failed writes of that run in a caution note of their own (the same sentence the feed carried, so a run that succeeded and still left a setpoint where it was says so here too), and the reminder "Runs execute on the actions lane, one at a time. Only a failed run reaches the Events console; a run that succeeds, and every value it writes, is kept for the detailed capture. Triggers invoke actions on schedule." The rail belongs to the live action: an autosave never blanks it; a restart of the runtime does.
Fired by
Under the duration, the line "Fired by: …" says what ran the action last:
| What ran it | The line reads |
|---|---|
| Run on this page | "Fired by: Run on this page" |
| A dashboard Button | "Fired by: A dashboard button" |
The agent (action_run) |
"Fired by: Agent" |
| A trigger | The trigger's name: "Fired by: Heater off when too hot". |
| A Process run's Logic action command | The unit and the action group: "Fired by: Process · Oven 1 · After soak". |
Used by
The Used by card lists everything in the station that runs the action ("Everything in the station that runs this action."), read when the action is opened: one row per Process command that fires it, per trigger bound to it and per dashboard Button that runs it.
| Kind | Location | Name |
|---|---|---|
| Process command | The model. | The action group and the moment: After soak · Completion. |
| Trigger | Empty. | The trigger, under its folders. |
| Button | The dashboard. | The Button's caption. |
With nothing, it says "Nothing in the station runs this action."; when the references cannot be read, "This action may still be in use. Try again before deleting or renaming it." with Try again. The card only reads, so every role sees it, also while the station is locked in engineering. Read it before disabling or deleting an action: a Process command whose action is gone runs nothing and says so on its own page.
Reading the run that fired it
An action a Process run fires at Completion reads that run under the unit's LastRun branch, which holds it by the time the action runs; one fired at Start reads the starting run's metadata under Procedure.Metadata. Process points read as plain values, with no .Value after them, unlike variables, and a point the run does not hold reads null, so guard it:
if (Process.Oven_soak.Oven_1.LastRun.Verdict === "Ok") {
Variables.Last_serial = Process.Oven_soak.Oven_1.LastRun.Metadata.Serial;
}
const peak = Process.Oven_soak.Oven_1.LastRun.Evaluations.Soak_check.Temperature_in_range.MaxValue;
if (peak !== null) {
Variables.Last_peak = peak;
}
The addresses are written out whole, as every address of a script is: an address cannot be built from a variable.
Commands
| Command | What it does | Greyed when | Not drawn when |
|---|---|---|---|
| Run | Saves the draft (with Configure logic) and queues one run. | Runtime stopped. | Role lacks Operate logic. |
| Enable / Disable | Flips Enabled; a disabled action refuses Run and idles every trigger bound to it, and a Process command that fires it is refused each time and reads Review. | Station locked or read-only. | Role lacks Configure logic. |
| Copy | Duplicates the action to the Actions root under a free name, marked in that root's table rather than opened (Creating and copying); the copy has no run recorded and no trigger bound to it. | Station locked or read-only. | Role lacks Configure logic. |
| Delete action | Asks "Delete 'X'? This removes the action. This cannot be undone." A trigger bound to it is left unbound, not deleted; a Process command that runs it stays, runs nothing and reads Review. | Station locked or read-only. | Role lacks Configure logic. |
The Actions root panel lists every action with its state in the Value column: "Enabled · last run <when>", with "· error" when the last run failed, "· write failed" when it succeeded and a write of its did not, or "Disabled".
What actions do not do
They do not dispatch Process run commands, stage a procedure or add a run comment (those points are read-only readiness and staging values); they do not acknowledge alarms; they do not run in parallel; they keep no state between runs and share no functions; and they are not fired by value changes or conditions, only by Run, a trigger's interval, a dashboard button, the agent or a Process run's command. A Process run never waits for the action it fires, and the action's outcome is never written into the run.