Script Engine SDK
Access the Store, Historian, and external systems from JavaScript scripts.
The Script Engine runs sandboxed JavaScript programs on each ControlBird node. Scripts have direct, in-process access to the Store for reading and writing entity data, to the Historian for time-series queries, and to a console API for logging. Programs are authored and tested in the Script Manager app, scheduled with cron via ProgramScheduler entities, or triggered by field changes via ProgramNotifier entities.
Execution model
Scripts execute synchronously. There is no event loop, no setTimeout/setInterval, and no fetch. Store operations appear synchronous to your script: you call store.read(...) and the value is returned immediately.
How a script runs
Every program is an entity. Execution is triggered by writing any value to the Execute field (the value itself is ignored). The engine loads the Source field, runs it, and records the outcome in the program's execution statistics.
- Create a
Programentity in Script Manager or withstore.create('Program'). - Edit the
Sourcefield with JavaScript that uses thestore,historian, andconsoleAPIs. - Set the
Argsfield with initial JSON state (or leave it empty). - Write any value to
Executeto run the program. - Read
LastExecutionStatus,LastExecutionTime, andLastErrorto observe the result. - Review the program's own log output for
consolemessages and diagnostics.
Program entities
The Script Engine recognizes four entity types. A Program holds source code. A RuleChain compiles a visual graph to JavaScript and runs the same way. The two trigger types start a program automatically.
| Entity | Purpose |
|---|---|
Program | Executable JavaScript with source, arguments, and execution statistics. |
RuleChain | Visual rule chain that auto-compiles to JavaScript and executes like a Program. |
ProgramScheduler | Runs a Program on a cron schedule (leader node only). |
ProgramNotifier | Runs a Program when a watched field is written. |
Program fields
| Field | Description |
|---|---|
Name | Program name (also used to label its log output). |
Description | Human-readable description. |
Source | JavaScript source code. |
Args | JSON state read and written atomically each execution. |
Execute | Write any value to trigger a run. |
LastExecutionTime | Timestamp of the most recent run. |
LastExecutionStatus | 0 = Running/Unknown, 1 = Success, 2 = Error. |
LastError | Error message from the most recent failed run. |
TotalExecutions | Total number of runs. |
GoodExecutions | Number of successful runs. |
BadExecutions | Number of failed runs. |
Value wrapper types
The Store is strongly typed. Field values are passed and returned as single-key wrapper objects so the engine knows the exact type. Use the matching wrapper when writing, and read the matching key when reading.
| Wrapper | Example | Meaning |
|---|---|---|
Bool | { Bool: true } | Boolean. |
Int | { Int: 42 } | Integer. |
Float | { Float: 3.14 } | Floating point. |
String | { String: 'text' } | UTF-8 string. |
Timestamp | { Timestamp: seconds } | Seconds since epoch. |
Choice | { Choice: index } | Enumerated choice by index. |
EntityReference | { EntityReference: id } | Reference to a single entity. |
EntityList | { EntityList: [id1, id2] } | Ordered list of entity IDs. |
Decimal | { Decimal: string } | Arbitrary-precision decimal as a string. |
Blob | { Blob: bytes } | Raw binary data. |
store API
Field and entity types are referenced by resolved type handles. Resolve them once with getFieldType and getEntityType, then pass the resolved handles to read and write operations.
Type resolution
const nameField = store.getFieldType('Name');
const deviceType = store.getEntityType('Device');Reading and writing fields
Read one or more fields from an entity, then unwrap the typed value.
// Read and log an entity name
const nameField = store.getFieldType('Name');
const result = store.read('12345', [nameField]);
console.log('Entity name:', result.value.String);Write multiple fields atomically with writeMulti.
const statusField = store.getFieldType('Status');
const tempField = store.getFieldType('Temperature');
store.writeMulti(entityId, {
'Status': { Int: 1 },
'Temperature': { Float: 23.5 }
});Creating and deleting entities
const deviceType = store.getEntityType('Device');
const newDeviceId = store.create(deviceType, null, 'NewDevice');
console.log('Created device:', newDeviceId);Finding entities with CEL filters
find returns the IDs of entities of a given type that match a CEL filter expression. Related helpers include queryChildren, getEntityTypes, and resolvePath.
const sensorType = store.getEntityType('Sensor');
const activeSensors = store.find(sensorType, 'Status == 1');
for (const sensorId of activeSensors) {
console.log('Active sensor:', sensorId);
}Store operations
| Operation | Description |
|---|---|
read / write | Read or write field values on an entity. |
writeMulti / pipeline | Write multiple fields atomically in one operation. |
create / delete | Create or delete entities. |
getEntityType / getFieldType | Resolve type handles from names. |
find | Query entities of a type with a CEL filter. |
queryChildren / getEntityTypes | Discover child entities and registered entity types. |
resolvePath | Resolve a hierarchical path to an entity ID. |
getCompleteEntitySchema / updateSchema | Introspect and modify entity schemas. |
callRuleChain | Invoke another rule chain with input and receive output. |
historian API
Query recorded time-series data with historian.query. Time bounds are given in milliseconds. Each returned record carries a timestamp, entity ID, field path, the typed value, the writer name, and context.
// Last hour of temperature samples, up to 100 records
const records = historian.query(
'temperature_table',
Date.now() - 3600000,
Date.now(),
100,
null
);
for (const record of records) {
console.log(`${record.timestamp}: ${record.value.Float}`);
}Historian must be connected
historian.query only returns data when the Historian service is connected and recording. See the Historian walkthrough for setup.
console API
Use console.log for debugging output. Each program writes to its own log file, which rotates automatically so logs never grow without bound.
console.log('Script started');
console.log('Active sensors:', activeSensors.length);Persistent state with Args
Scripts have no global state between runs. The single exception is the Args field, which holds JSON that is read at the start of execution and written back at the end. Use it to carry counters, cursors, or configuration across runs. Leave it empty if you do not need persistence.
Calling rule chains
A script can invoke another rule chain with callRuleChain, passing input and receiving output. Call depth is bounded to prevent runaway recursion, and the target RuleChain must have been compiled before it can be called.
Triggering programs
Manual and Execute trigger
Run a program from the Script Manager test panel, or write any value to its Execute field from any service or script.
Scheduled execution
A ProgramScheduler references a Program and runs it on a CronExpression when Enabled. Scheduling uses leader election: only the leader node executes scheduled tasks, so a program runs once across the cluster regardless of node count.
// CronExpression examples
'0 9 * * *' // 9:00 AM daily
'*/5 * * * *' // every 5 minutesEvent-triggered execution
A ProgramNotifier watches a field and runs a linked Program when that field is written. Configure it with the target field and trigger mode.
| Field | Description |
|---|---|
NotifyEntityType / NotifyEntityId | Which entity to watch. |
NotifyField | Which field on that entity to watch. |
NotifyTriggerOnChange | true = only on value change; false = on every write. |
NotifyContext | Related fields delivered atomically with the trigger. |
Program | The Program to execute. |
Enabled | Whether the notifier is active. |
Limitations
The runtime is deliberately sandboxed. The following are not available to scripts:
- No
fetchor arbitrary HTTP requests. - No timers,
setTimeout, orsetInterval(execution is synchronous). - No external module imports beyond what is built into the runtime.
- No process spawning and no direct filesystem access.
- No real-time field subscriptions; runs start only via
Execute,ProgramScheduler, orProgramNotifier. - No global state between executions except the
Argsfield. - Rule chain recursion is bounded to prevent runaway calls.
Author and test in Script Manager
The Script Manager app provides editing, manual test runs, and live status, so you can iterate on a program before wiring it to a scheduler or notifier. See the scripting walkthrough for a guided tour.