Scripting
Control Seat has three scripting environments. They share some concepts (tags, props, params, the object model) but each has its own context and capabilities.
1. Binding Scripts
Binding scripts run inside the binding engine in both editor preview and runtime. They execute whenever a bound source value changes and can return a transformed value or trigger side effects.
Context Variables
Every binding script receives these variables automatically:
value source value (scalar for single-source, object for multi-source)
tags tag store — tags.read(path), tags.write(path, value)
self this component as an object-model node
page the page root as an object-model node
For multi-source bindings, value is an object keyed by source name:
// Single source: value is the tag value directly
if (value > 100) return "HIGH";
// Multi-source: value is { sourceName: tagValue, ... }
const temp = value.temperature;
const pressure = value.pressure;
return temp > 80 && pressure > 50 ? "DANGER" : "OK";
Props & Params
Props are custom key-value data stored on components. Params are view-level parameters used by embedded views.
// Read props
prop("key") // read from this component
prop("#id", "key") // read from another component by ID
props() // all props on this component
props("#id") // all props on another component
// Write props
setProp("key", value) // set on this component
setProp("#id", "key", value) // set on another component
// Read params
param("name") // read param from this component
param("#id", "name") // read param from another component
// Write params
setParam("name", value) // set param on this component (embedded-safe)
setParam("#id", "name", value) // set param on another component
Nested Data
Props and params are stored as JSON and can contain nested objects and arrays. All prop, param, setProp, and setParam calls support deep paths using dot notation and bracket syntax:
// Given a component with props:
// { config: { limits: [{ min: 0, max: 100 }, { min: 10, max: 200 }], unit: "°F" } }
prop("config.unit") // "°F"
prop("config.limits[0].max") // 100
prop("config.limits[1].min") // 10
// Write nested values — missing intermediates are created automatically
setProp("config.limits[0].max", 150)
setProp("config.alerts.high", true) // creates { alerts: { high: true } }
// Same syntax works for params
param("style.colors[0]")
setParam("style.colors[0]", "#ff0000")
Finding Components
These helpers search the page for components by name, ID, or type:
find("Coordinate Canvas") // first match by component name
findAll("Chart") // all matches by name or type
byId("isk7c") // find by component ID
byName("myPump") // find by component meta-name
Object Model (Node API)
Every component found through self, page, find(), byId(), etc. is wrapped in a node object with a consistent API:
// Identity
node.id // component ID
node.name // meta-name (data-cs-meta-name)
node.type // component type
node.element // underlying DOM element
// Read data
node.attrs() // all HTML attributes as { name: value }
node.props() // all custom props
node.prop("key") // single prop
node.prop("config.limits[0].max") // nested path with dot + bracket notation
node.styles() // inline/component styles
node.params() // all params
node.param("color") // single param
// Write data
node.setProp("key", value) // set a custom prop
node.setProp("config.limits[0].max", 100) // set nested value (creates intermediates)
node.setParam("name", value) // set a param (embedded-safe, chainable)
// Navigate the tree
node.children() // all direct children as wrapped nodes
node.children("[selector]") // filtered children (CSS selector)
node.get("ref") // first descendant matching name/type
node.all("ref") // all descendants matching name/type
Nodes are iterable — you can loop over children directly:
for (const child of node) {
console.log(child.name, child.param("color"));
}
Binding Script Examples
Threshold alarm — return a label based on value:
const max = prop("maxLevel");
if (value > max) return "HIGH";
if (value > max * 0.8) return "WARNING";
return "NORMAL";
Color a component based on a tag value:
if (value > 100) return "red";
if (value > 50) return "orange";
return "green";
Walk embedded views and set params:
const canvas = self.get("Coordinate Canvas");
if (!canvas) return;
for (const view of canvas.children("[data-cs-view]")) {
const temp = view.param("temperature");
view.setParam("color", temp > 80 ? "red" : "blue");
}
Multi-source — combine two tag values:
// value = { flow: 42.5, pressure: 1013 }
const { flow, pressure } = value;
return `Flow: ${flow.toFixed(1)} | Pressure: ${pressure.toFixed(0)}`;
Read props from another component:
const threshold = prop("#configPanel", "alarmThreshold");
if (value > threshold) {
setProp("status", "ALARM");
return "red";
}
setProp("status", "OK");
return "green";
Find all charts and update their colors:
const charts = findAll("Chart");
for (const chart of charts) {
chart.setProp("borderColor", value > 50 ? "#ff4444" : "#44ff44");
}
2. Page Scripts
Page scripts are custom JavaScript attached to a page. They have access to tags, the object model, and component props/params.
Available Variables
Page scripts have these variables available directly:
// Tags
tags.read("default.temperature") // read a tag value
tags.write("default.setpoint", 72) // write a tag value
tags.subscribe("default.temperature", (value) => {
// called whenever the tag value changes
})
// Props (always require an ID — page scripts have no "current component")
prop("#myComponent", "key") // read a prop
props("#myComponent") // all props
setProp("#myComponent", "key", value) // set a prop
// Params
params() // all page param values
params("name") // single param value
setParam("name", value) // set a page param
// Object model
self() // page root as a node
page() // same as self()
find("Coordinate Canvas") // first match by name/type
findAll("Chart") // all matches
byId("myComponent") // find by ID
byName("myPump") // find by meta-name
Tag Subscriptions
tags.subscribe is the primary way to react to live tag changes. Subscriptions are automatically cleaned up when the page navigates away.
// Subscribe to a single tag — callback receives the new value
tags.subscribe("default.temperature", (value) => {
byId("tempDisplay").setProp("value", value);
});
// Subscribe to all tag changes
tags.subscribe((state) => {
console.log("Tag state changed:", state);
});
Tag paths use dot notation: "default.temperature", "plc.motor.speed", "site.building1.room3.humidity".
Object Model in Page Scripts
The same node API from binding scripts is available in page scripts.
// Find a component and read its data
const pump = byId("pump1");
console.log(pump.name); // "Main Pump"
console.log(pump.props()); // { status: "running", speed: 75 }
console.log(pump.param("color")); // "blue"
// Update props on a component
const display = byName("StatusDisplay");
display.setProp("label", "Online");
display.setParam("color", "green");
// Walk the component tree
const canvas = find("Coordinate Canvas");
for (const view of canvas.children("[data-cs-view]")) {
console.log(view.name, view.params());
}
Page Script Examples
Subscribe to a tag and update a display:
tags.subscribe("default.temperature", (value) => {
const display = byId("tempDisplay");
display.setProp("value", value.toFixed(1) + "°F");
display.setParam("color", value > 100 ? "red" : "green");
});
Animate a progress bar based on a tag:
tags.subscribe("default.tank_level", (level) => {
const bar = byId("levelBar");
bar.element.style.width = `${Math.min(level, 100)}%`;
bar.element.style.background = level > 90 ? "#ff4444" : "#4488ff";
});
Read from multiple tags and compute a derived value:
tags.subscribe("default.flow_rate", (flow) => {
const pressure = tags.read("default.pressure");
const efficiency = flow / Math.max(pressure, 1);
byId("efficiency").setProp("value", efficiency.toFixed(2));
});
Set page params and cascade to embedded views:
tags.subscribe("default.active_zone", (zone) => {
setParam("activeZone", zone);
const canvas = find("Coordinate Canvas");
for (const view of canvas.children("[data-cs-view]")) {
view.setParam("highlighted", view.name === zone);
}
});
Find all charts and log their configuration:
const charts = findAll("Chart");
for (const chart of charts) {
console.log(chart.name, chart.props("historianTagPath"));
}
Write a tag value (e.g., user sets a setpoint):
const slider = byId("setpointSlider");
// Assuming the slider dispatches change events
slider.element.addEventListener("input", (e) => {
tags.write("default.setpoint", Number(e.target.value));
});
Important Differences From Binding Scripts
- Page scripts do not have a "current component."
prop("key")andsetProp("key", value)without an ID won't work — always pass an explicit ID likeprop("#pump1", "status"). - Page scripts do not receive a
valuevariable. Usetags.read()ortags.subscribe()to get tag values. - Page scripts run once when the page loads. Use
tags.subscribe()for live updates.
3. Gateway Scripts
Gateway scripts run server-side in the Node gateway worker. They are used for automation, alarms, data processing, and integrations that should not depend on a browser being open.
Entry Point Structure
Every gateway script entry has a triggers array and a default export function:
export const triggers = [onManual()];
export default function main({ tags }) {
// your logic here
}
Triggers
Triggers define when the entry runs:
// Run manually from the UI
onManual()
// Run when a tag value changes
onTagSubscribe("default.motor.speed")
// Run on a fixed interval
onInterval("1s") // every second
onInterval("5m") // every 5 minutes
onInterval("1h") // every hour
// Run on a cron schedule
onSchedule("0 0 * * *") // midnight daily
onSchedule("*/5 * * * *") // every 5 minutes
onSchedule("0 8 * * 1") // Monday at 8am
// Run when a condition becomes true
onCondition('tags.read("default.temp") > 90')
// Run on an HTTP request
onHttp("/hooks/my-webhook")
Multiple triggers can be combined:
export const triggers = [
onManual(),
onTagSubscribe("default.motor.speed"),
onInterval("30s"),
];
Context Object
The main function receives a context object whose contents depend on the trigger:
export default function main(ctx) {
ctx.tags // tag store — tags.read(path), tags.write(path, value)
ctx.tag // (onTagSubscribe) the tag path that changed
ctx.value // (onTagSubscribe) new value
ctx.previous // (onTagSubscribe) previous value
ctx.tick // (onInterval) tick count
ctx.scheduledAt // (onSchedule) scheduled timestamp
ctx.condition // (onCondition) the condition expression
ctx.request // (onHttp) { method, path, headers, body }
}
Modules
Helper code can be split into modules and imported by entries:
// modules/helpers.js
export function celsiusToFahrenheit(c) {
return c * 9 / 5 + 32;
}
// entries/main.js
import { celsiusToFahrenheit } from "./helpers.js";
export const triggers = [onTagSubscribe("default.temp_c")];
export default function main({ tags, value }) {
const tempF = celsiusToFahrenheit(value);
tags.write("default.temp_f", tempF);
}
Gateway Script Examples
Tag change alarm — write an alarm tag when a threshold is exceeded:
export const triggers = [onTagSubscribe("default.tank_level")];
export default function main({ tags, value, previous }) {
const high = value > 90;
const wasHigh = previous > 90;
if (high && !wasHigh) {
tags.write("alarm.tank_high", true);
tags.write("alarm.tank_high_timestamp", new Date().toISOString());
} else if (!high && wasHigh) {
tags.write("alarm.tank_high", false);
}
}
Periodic aggregation — compute a rolling average every 30 seconds:
export const triggers = [onInterval("30s")];
export default function main({ tags }) {
const temp1 = tags.read("default.zone1.temp") || 0;
const temp2 = tags.read("default.zone2.temp") || 0;
const temp3 = tags.read("default.zone3.temp") || 0;
const avg = (temp1 + temp2 + temp3) / 3;
tags.write("default.avg_temp", Math.round(avg * 100) / 100);
}
Condition-based automation — shut down a pump when pressure is too high:
export const triggers = [
onCondition('tags.read("default.pressure") > 150'),
];
export default function main({ tags }) {
tags.write("default.pump.command", "STOP");
tags.write("alarm.overpressure", true);
}
Webhook handler — accept external data via HTTP:
export const triggers = [onHttp("/hooks/sensor-data")];
export default function main({ tags, request }) {
const { body } = request;
if (body && body.temperature !== undefined) {
tags.write("external.temperature", body.temperature);
}
if (body && body.humidity !== undefined) {
tags.write("external.humidity", body.humidity);
}
}
Cron job — daily report snapshot:
export const triggers = [onSchedule("0 6 * * *")]; // 6am daily
export default function main({ tags }) {
const snapshot = {
timestamp: new Date().toISOString(),
production: tags.read("default.daily_production") || 0,
uptime: tags.read("default.uptime_hours") || 0,
alarms: tags.read("default.alarm_count") || 0,
};
tags.write("reports.daily_snapshot", JSON.stringify(snapshot));
}
Multi-trigger entry — manual test + live monitoring:
export const triggers = [
onManual(),
onTagSubscribe("default.motor.current"),
];
export default function main({ tags, tag, value }) {
// If triggered manually, just log current state
if (!tag) {
const current = tags.read("default.motor.current");
console.log("Motor current:", current);
return;
}
// If triggered by tag change, check for overcurrent
if (value > 15) {
tags.write("alarm.motor_overcurrent", true);
tags.write("default.motor.command", "REDUCE_LOAD");
}
}
Known Behaviors
- Multi-source key collisions:
valuein multi-source bindings is keyed by tag leaf name. Two sources ending with the same token (e.g.,zone1.tempandzone2.temp) will collide undervalue.temp. - Return value sanitization: Binding script return values are sanitized. DOM nodes, functions, and circular objects are stripped. Return plain values or serializable objects.
- Embedded view params:
setParamon embedded views updates both host props and the embedded-view param bridge, so it is safe for instance-local overrides. - Re-entry guard: Binding scripts suppress nested
setProp/setParamcalls that would cause immediate re-evaluation, preventing infinite loops. - Automatic cleanup: In page scripts,
tags.subscribe,setTimeout,setInterval,addEventListener, andrequestAnimationFrameare automatically tracked and cleaned up on page navigation. - Tag path format: Tags use dot-notation paths like
"default.temperature". Paths without a prefix are automatically prefixed with"default.".