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.

  1. Create a Program entity in Script Manager or with store.create('Program').
  2. Edit the Source field with JavaScript that uses the store, historian, and console APIs.
  3. Set the Args field with initial JSON state (or leave it empty).
  4. Write any value to Execute to run the program.
  5. Read LastExecutionStatus, LastExecutionTime, and LastError to observe the result.
  6. Review the program's own log output for console messages 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.

EntityPurpose
ProgramExecutable JavaScript with source, arguments, and execution statistics.
RuleChainVisual rule chain that auto-compiles to JavaScript and executes like a Program.
ProgramSchedulerRuns a Program on a cron schedule (leader node only).
ProgramNotifierRuns a Program when a watched field is written.

Program fields

FieldDescription
NameProgram name (also used to label its log output).
DescriptionHuman-readable description.
SourceJavaScript source code.
ArgsJSON state read and written atomically each execution.
ExecuteWrite any value to trigger a run.
LastExecutionTimeTimestamp of the most recent run.
LastExecutionStatus0 = Running/Unknown, 1 = Success, 2 = Error.
LastErrorError message from the most recent failed run.
TotalExecutionsTotal number of runs.
GoodExecutionsNumber of successful runs.
BadExecutionsNumber 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.

WrapperExampleMeaning
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

OperationDescription
read / writeRead or write field values on an entity.
writeMulti / pipelineWrite multiple fields atomically in one operation.
create / deleteCreate or delete entities.
getEntityType / getFieldTypeResolve type handles from names.
findQuery entities of a type with a CEL filter.
queryChildren / getEntityTypesDiscover child entities and registered entity types.
resolvePathResolve a hierarchical path to an entity ID.
getCompleteEntitySchema / updateSchemaIntrospect and modify entity schemas.
callRuleChainInvoke 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 minutes

Event-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.

FieldDescription
NotifyEntityType / NotifyEntityIdWhich entity to watch.
NotifyFieldWhich field on that entity to watch.
NotifyTriggerOnChangetrue = only on value change; false = on every write.
NotifyContextRelated fields delivered atomically with the trigger.
ProgramThe Program to execute.
EnabledWhether the notifier is active.

Limitations

The runtime is deliberately sandboxed. The following are not available to scripts:

  • No fetch or arbitrary HTTP requests.
  • No timers, setTimeout, or setInterval (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, or ProgramNotifier.
  • No global state between executions except the Args field.
  • 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.