Skip to main content
Version: 0.1

Cron scripts

A cron job (from the cron module, io.scadex.cron) runs either a single tag write or a script on a schedule. This page covers the script kind: the context it gets, how it is attributed, and what the gateway keeps of each run. See Scripting for the sandbox and The system API for what system.* offers.

How a cron script runs​

The cron module hands the script to the gateway's module script service (module contract 1.3). That means:

  • Limits are the module limits: 2,000,000 statements, 32 MiB and 5 s of CPU per run. Module scripts take turns, one at a time across the gateway; a run waits at most 30 s for its turn and otherwise fails with "Another module script ran for too long for this one to get a turn."
  • The script runs as the gateway's own account with the engineer role, so system.db.* and the tag and alarm calls work, but system.user.list() (admin) does not.
  • Writes are collected while the script runs and applied after it finishes, in order, each with a 15 s timeout, stopping at the first failure. A script that fails writes nothing.
  • console.log output is kept with the run (at most 200 lines and 64 KB).

A gateway older than contract 1.3 cannot run module scripts; the run then fails with "This gateway doesn't run scripts for modules. It needs Scadex 1.3 or later."

The job context​

GlobalWhat it holds
event.nameThe job's name
event.params.triggerWhy it runs: schedule, manual (someone pressed run), or catch-up (a run missed while the gateway was down)
event.params.scheduledForThe time the schedule named, ISO 8601. Absent on a test run
event.params.jobThe job's name again
event.params.jobIdThe job's id. Absent on a test run
event.params.runIdThe run record's id. Absent on a test run
tag(path), tags, writeTag(path, value)As in every script. A cron script has no screen, so {param} placeholders have nothing to fill them
valundefined
system.*All of the system API, as an engineer

The editor types trigger and scheduledFor; the other parameters are plain strings on event.params.

Attribution and auditing​

Each write the script makes is audited under the run's principal and a correlation id of cron-run-<run id>:

  • A scheduled or caught-up run writes as cron:<job name>.
  • A manual run (Run now, or POST .../jobs/{id}/run) writes as the user who started it.

system.user.current() returns that principal; for a scheduled run it is no person, so name is the principal id and email is empty.

The run record​

Every run is recorded and shown on the job. Its detail keeps:

FieldContent
outcomerunning, then succeeded or failed. A run that never started is skipped (the previous run was still going) or missed (the gateway was down)
errorThe script's error, or the first write's failure, when it failed
valueWhat the script returned, as JSON. NaN and Infinity are kept as text
logsThe console.* lines with their level
writesEach write with its path, value, whether it was applied, and its error

A job can name a failure tag: a boolean tag the module writes as cron:<job name>, true after a failed run and false after a successful one, only when that differs from what it last wrote. If that write is refused, the run gets a note saying so. The Cron jobs page shows this under On failure.

Testing a script​

POST /api/v1/modules/io.scadex.cron/test takes the same body as creating a job and runs its action once as a dry run, without keeping a record: the script runs for real (reads, system.* queries), but its writes are reported with applied: false rather than made, and changing system.* calls (acknowledging, shelving, database updates in a transaction that is rolled back) are listed in the logs as Dry run, not done: .... The response is a run shaped like a recorded one, with the note "A trial: nothing was written.", trigger: manual and event.params without jobId, runId or scheduledFor. The Try it button in the job form on the Cron jobs page runs this same dry run.

The script console is another way to try the same code as yourself; see Script console.

Examples​

Reset a daily counter at midnight (schedule the job for 00:00):

var before = tag('Line1.DailyCount');
writeTag('Line1.DailyCount', 0);
console.log('Reset from ' + before);
return before;

Copy a value from one tag to another, recording when:

writeTag('Reports.Shift.EndLevel', tag('Plant.Tank1.Level'));
writeTag('Reports.Shift.EndLevelAt', system.date.format(system.date.now()));

Compute yesterday's total from history and store it (system.tag.history returns at most 10,000 points, oldest first):

var today = system.date.startOfDay(system.date.now());
var yesterday = system.date.addDays(today, -1);
var points = system.tag.history('Line1.Flow', { from: yesterday, to: today, maxPoints: 10000 });
var total = 0;
for (var i = 1; i < points.length; i++) {
var seconds = (points[i].t - points[i - 1].t) / 1000;
if (points[i - 1].v !== null) total += points[i - 1].v * seconds;
}
writeTag('Line1.FlowTotalYesterday', system.util.round(total, 2));
return { points: points.length, total: total, day: system.date.format(yesterday, 'yyyy-MM-dd') };

Write a summary row to an outside database (see Databases and storage):

var standing = system.alarm.active();
system.db.update(
'INSERT INTO daily_status (day, standing_alarms, level) VALUES (:day, :alarms, :level)',
{ day: system.date.format(system.date.now(), 'yyyy-MM-dd'), alarms: standing.length, level: tag('Plant.Tank1.Level') },
'Reporting'
);
return standing.length;