Skip to content

Logic & the Logic Cycle

In JasperNode you don’t write a program that runs top-to-bottom. You attach small scripts to individual tags, and the runtime runs each one only when something it depends on changes. This page explains that model, the API you write against, and the cycle that executes it.

A tag script is a short async JavaScript function stored on a tag. The single most important rule:

A script may set only its own tag’s value — by returning a value, or by calling self.setValue(...).

It cannot write any other tag. To influence another tag, you write your own and let that tag’s readers react. This is what keeps data flow explicit and traceable (and what lets the system test and reason about changes safely).

A script reads other tags by calling two helpers in the code, each with a full tag id (path/name) written as a plain string:

  • on("path/name") — returns the tag’s current value and makes it a trigger: whenever that tag changes, this script runs.
  • read("path/name") — returns the tag’s current value as a read-only input that does not run the script.

Both return the value directly — if temp_c is a number, on("factory/line1/temp_c") is that number.

You don’t maintain a separate list of triggers anywhere. The wiring is read straight out of your code: every on(...) and read(...) call site is scanned when you deploy, and that scan is the dependency graph. Change the code and the wiring follows automatically — the two can never drift out of step.

Two consequences worth knowing:

  • The path must be a plain string literalon("factory/line1/temp_c"), never on(someVar) or a built-up string. A dynamic path can’t be read without running the script, so it is rejected at deploy with the offending line named. (Branches are fine: put a literal in each, cond ? on("a/b") : on("c/d").)
  • Draft vs deployed. As you type, the Inputs for testing panel reflects your draft code immediately — a newly typed on(...) shows up right away. But the Connections tab and Flow Exploration only change once you Deploy: they show the live dependency graph, not your in-progress edits.

Here is a real script — convert a Celsius reading to Fahrenheit:

// Tag: factory/line1/temp_f
const c = on("factory/line1/temp_c"); // trigger + read: runs whenever temp_c changes
return (c * 9) / 5 + 32;
  • Calling on("factory/line1/temp_c") does two things at once: it returns the current Celsius value and wires temp_c as a trigger, so temp_f recomputes every time temp_c changes.
  • The return value becomes factory/line1/temp_f’s new value.

A second example — a counter that ticks 0→15 and wraps, once per second:

// Tag: demo/counter
on("__sys/host/clock/second"); // trigger only — we need the tick, not its value
if (typeof cache.count !== "number") cache.count = -1;
cache.count = (cache.count + 1) & 0xf; // wrap 0–15
return cache.count;
  • It runs once per second because the 1 Hz host clock __sys/host/clock/second is its only trigger. Here we call on(...) purely to subscribe — we ignore the returned value.
  • cache is a per-script object that persists between runs, so the count carries over.

When you write a script body, these are available to you:

NameWhat it is
selfyour own tag — read it, and write it
onon("path/name") — read a tag’s value and make it a trigger
readread("path/name") — read a tag’s value without triggering
scriptthe firing context — what triggered this run (script.caller, script.callerIs(…), …)
fnengine helpers (timers, file reads)
cachea per-script object that survives across runs (timers, counters, latches)
consolelogging that routes to the node’s log buffers
  • self.value / self.getValue() — the current value.
  • self.setValue(v) — set your tag’s value imperatively (equivalent to return v).
  • self.name, self.path — identity.
  • self.statsprevValue, ts, prevTs, source, …
  • script.caller — the full tag id of the trigger that fired this run, or a sentinel: "run_once" (you clicked Run once), "resume" (the operator restarted the Logic Cycle), or "" (never run / isolated test).
  • script.callerIs("path/name") — true when that tag is what fired this run. Use it to branch on which input changed, e.g. handle a reset trigger differently from a normal update.
  • script.inputs() / script.ons() / script.reads() — the values this run started with, as a list.
  • fn.sleep(ms)await fn.sleep(250) pauses this run (for debouncing, pulsing, sequencing).
  • fn.timerOn(ms, valueWhenElapsed, defaultValue?) — a latching on-delay timer (PLC TON). Call it while a condition holds: it returns defaultValue until the condition has held for ms, then latches and returns valueWhenElapsed. Not calling it on a run resets the timer. One timer per script; ms and valueWhenElapsed are fixed when the timer starts.
  • fn.readFile(path) / fn.readFileBytes(path) — read a file from the host.

cache is how a script remembers things: a running count, a previous edge, a timer’s bookkeeping. It lives only in memory (attach it to nothing you need persisted across a restart).

The values are snapshotted when the run starts

Section titled “The values are snapshotted when the run starts”

When a script starts, JasperNode takes one atomic snapshot of every tag it declared — its input image, exactly like a PLC latching its inputs at the top of a scan. For the rest of that run, on(...) and read(...) return those snapshot values, even after an await. Calling the same on(...) twice in one run always gives the same number. If a script needs fresher values, it simply runs again on the next trigger. This removes a whole class of races where two reads of related tags could disagree mid-run.

A deployed script can carry a one-line description — a plain-language caption of what it does, kept to one short sentence (120 characters maximum). It is not a comment in the code; it is a property of the script, and it is what the Flow Exploration view prints on the tag’s chip so a flow reads as an explanation rather than a bare graph.

The description is bound to the code it was written for. If you change the script and redeploy, the old description stays but is flagged outdated — JasperNode knows the code moved on and the sentence may no longer be true. It never silently rewrites it for you, and it never blocks a deploy. To clear the flag, edit the text or press Auto generate.

Auto generate writes the sentence for you from the deployed code — “Recomputes when ‘factory/line1/temp_c’ changes; reads ‘factory/line1/setpoint’.” It is a local, offline summary derived by reading your on() / read() calls; it costs nothing and does not involve the AI agent.

Anything Jasper deploys arrives already described — the agent is required to supply a description with every script it writes, so AI-authored logic is never an unlabelled box on the flow. And descriptions travel with a project backup, so a restored script keeps its caption.

  • Read a tag it didn’t declare with on() or read() — calling on(...) / read(...) on a path that isn’t in the code is a script error. It guarantees the dependency graph is complete and honest, which is what makes chain-testing and AI analysis trustworthy.
  • Write any tag other than self — side effects must travel through the dependency graph, not through hidden writes.
  • Write self when an enabled connector owns it as an input — the field is the source of truth; the write is refused. (To inject values for simulation, disable the connector first — see the simulation pattern.)

Because a script writes only its own tag, behaviour propagates as an explicit chain:

factory/line1/temperature changes
└─▶ temp_alarm (calls on("…/temperature")) → computes true
└─▶ shutdown_request (calls on("…/temp_alarm")) → computes …

Each arrow is an on() dependency. You can follow these links in both directions in the Flow Exploration view, or on a tag’s Connections tab — pick a value and expand its upstream causes or downstream effects.

A worked example — a 5-second high-temperature alarm

Section titled “A worked example — a 5-second high-temperature alarm”

This shows fn.timerOn used as an on-delay: the alarm latches only after temperature has stayed above 80 for 5 continuous seconds, and clears immediately when it drops.

// Tag: factory/line1/temp_alarm
const temp = on("factory/line1/temperature"); // trigger + read
if (temp > 80) {
// Condition holds → run the on-delay. Returns false for the first 5 s, then true.
return fn.timerOn(5000, true, false);
}
// Condition cleared → don't call timerOn (this resets it) and report no alarm.
return false;

Downstream, a shutdown_request tag could call on("factory/line1/temp_alarm") and act on it — and you could test that whole chain before deploying.

The Logic Cycle — how scripts actually run

Section titled “The Logic Cycle — how scripts actually run”

Scripts execute in the Logic Cycle, JasperNode’s reactive runtime:

  • Event-driven. It sleeps until a trigger value changes, wakes, runs exactly the scripts whose triggers fired, then parks again. If nothing changed, nothing runs. (A one-second safety net bounds any missed wake.) The closest PLC analogy is an interrupt/event task, not the main scan.
  • Ordered by dependency distance. When a change fires several scripts, direct dependents run before indirect ones, so a chain settles in order.
  • Sandboxed and watchdog-protected. Scripts run isolated from the data engine, and any single run that exceeds 30 seconds is aborted. A buggy script cannot freeze the node.

You can pause and resume all scripts with the Logic Cycle Stop/Start control in the status bar — useful while reconfiguring. Per-script run stats (run count, average run time, last error, what triggered the last run, currently running) are shown on the tag’s Scripting tab.

The Scripting tab: editor, derived inputs, the test panel, and live run stats

When you inherit someone else’s logic, asking Jasper is usually faster than reading it: it can list what triggers a tag and summarise what the script does with them, side by side with the code.

Jasper listing a tag's triggers and explaining its state machine

The Scripting tab (on any user tag’s Properties panel) is where you author a script:

  • The code editor. Type your logic. As you write on("…") / read("…"), the editor autocompletes tag ids and you can drag a tag in from the tree.
  • Inputs for testing. A panel that lists the inputs it read out of your code — each on and read path, its live value, and a synthetic Test value. It’s read-only: you add or remove an input by editing on(...) / read(...) in the code, not with buttons here. A path that doesn’t match a real tag is flagged, because a deploy would reject it.
  • Test. Runs the script in an isolated copy of the engine against your synthetic inputs and shows what it would return — without touching any live tag.
  • Save / Deploy. Save stores your edits as a draft; the tag keeps running the live script until you Deploy, which promotes the draft atomically. Run once executes the deployed script a single time against live values.

Editing a script saves a draft; it doesn’t go live until you Deploy. Before deploying you can Test the saved script against synthetic input values without affecting the running process. A change the AI proposes goes further: it is tested in an isolated fork of the engine, and then flips live atomically. That workflow — the Deploy Gate and the guardrails on AI edits — is in The AI agent & Deploy Gate.

Deploying or enabling a script does not run it — a script only runs when one of its on() triggers changes, or when you click Run once to execute it a single time by hand. A consequence: a script with no on() inputs runs only via Run once. New scripts also default to Skip if already running, so a long run won’t pile up re-triggers behind it; you can change this per script (on the Advanced tab), or the node-wide default under System Config → Defaults for new items.

Need to drive an input tag that a connector normally owns — to test logic with no hardware attached? Disable the connector, then attach a script (or write from the UI) to the input tag; the engine accepts those writes while the connector is disabled. Re-enable the connector only after disabling the simulation script, or the two will fight over the tag. More in Connectors → Direction.