Connect IoT - Writing a Code Task Tutorial#
Overview#
The Code task runs a TypeScript class inside the controller, in a sandbox, each time it is activated. Every other task declares its ports and settings in its package, so the designer and a reader of the workflow know what it does. A Code task has the ports you add, does whatever its code says, and shows its failures only when it runs. Reach for it last, after the tasks and converters that already do the step:
| To | Use first |
|---|---|
| Compute a value from named inputs with a formula | Generic Expression Evaluator |
| Pick values out of a JSON object or reshape it | JSONata |
| Change one value on its way along a Data Flow link | A converter. See Converters. |
| Compute the value of one Control Flow row | An expression. See Control Flow Expressions. |
| Branch, loop, or retry | The Control Flow tasks. See Connect IoT - Control Flow in Depth Tutorial. |
| Read from or act on the equipment | On Equipment Event, Execute Equipment Command, and the other equipment tasks |
Table 1: Tasks to use before code
A Code task fits one small algorithm over the values that arrived on its inputs, such as parsing, grouping, validating, or assembling a nested object. The workflow keeps the wiring: what reacts to what, in which order, and every effect on the equipment.
This tutorial is for integrators who completed Connect IoT - How Values Travel Tutorial and Connect IoT - Control Flow in Depth Tutorial and can read TypeScript. You build a Data Flow page that reads each lot file, passes its text to a Code task, and logs a summary such as lot-a.txt: 2 lots [LOT-01, LOT-02], 2 since start. You then test the code with files that break its assumptions.
Info
For the settings, the class contract, and the services of the task, see Custom Code.
Before You Start#
- Set up the controller, the watched folder
C:/Lots/Incoming, and the fileslot-a.txtandlot-b.txtas in Connect IoT - Control Flow in Depth Tutorial. - Make sure that no other page of the controller reacts to
OnNewFile, so that no other workflow moves a file before this one reads it. Delete theFile Handlingpage of that tutorial, or set up a second controller the same way. -
Create three more test files outside the watched folder:
File Content, one value per line What it tests lot-c.txtLOT-05,LOT-06A file without ENDlot-d.txtNothing An empty file lot-e.txtLOT-07,Lot 8,ENDA line that is not a lot Table 2: Test files for the edge cases
-
Copy the files into the watched folder at each checkpoint, and remove them afterward: every file left in the folder is reported again when the connection starts.
Sequence of Steps#
Step 1: Wire a First Code Task and Run It#
Add a workflow page, select Data Flow, name the page Lot Summary, and add the following tasks:
| Task | Settings to set | Inputs/Outputs to link |
|---|---|---|
On Equipment Event | On Equipment Event OnNewFile | - |
Execute Equipment Command | Equipment Command ReadFile | path from the event FullPath; Activate from the event Success |
Code | On the Inputs tab, fileName and content, both of Type String. On the Outputs tab, summary (String) and count (Integer). | fileName from the event FileName; content from ReadFile reply, with the Any To String converter; Activate from ReadFile Success |
Log Message | Mode RawText | message from Code summary; Activate from Code Success |
Table 3: Tasks of the Lot Summary page
-
On the Code tab of the
Codetask, keep the class of the template and replace the body ofmain:import { Framework } from 'framework'; export default class { /** Allows accessing external functions */ private framework: Framework; constructor(framework: Framework) { this.framework = framework; } public async main(inputs: any, outputs: any): Promise<any> { const text: string = String(inputs.content ?? ""); return { summary: `${inputs.fileName}: ${text.length} characters` }; } } -
Save the Automation Controller, then copy
lot-a.txtintoC:/Lots/Incoming. The console showslot-a.txt: <length> characters.
The class is the default export, its constructor receives framework, and main(inputs, outputs) is the entry point, whose signature you keep. Inside the code, use the names you declared: inputs.content, and the key summary in the returned object. Outside, the task stores them as the ports contentIn and summaryOut. Each key of the returned object that names an output is emitted, then Success. A key that names no output is dropped, and an output that the object does not name is not emitted. Log Message writes only when its Activate input receives a value, so the message link alone logs nothing.
fileName reaches the Code task as soon as the event fires, and content arrives after the read. Both belong to the execution context of the same file, so they meet in one run. Values that come from two different triggers belong to two execution contexts and never meet in one run.
Note
In Control Flow, the Code block takes its inputs as rows, and the blocks after it read its outputs through expressions. Type a row String for text: an Object row parses text as JSON and fails.
Step 2: Parse the Lines and Reject Bad Data#
Replace the body of main with the parser. It reads lines up to END, skips blank lines, and throws an error for a line that is not a lot:
public async main(inputs: any, outputs: any): Promise<any> {
const fileName: string = inputs.fileName ?? "unknown file";
const lots: string[] = [];
for (const line of String(inputs.content ?? "").split("\n")) {
const value: string = line.trim();
if (value === "END") {
break;
}
if (value === "") {
continue;
}
if (!/^LOT-\d+$/.test(value)) {
throw new Error(`Unexpected line '${value}' in ${fileName}`);
}
lots.push(value);
}
return {
summary: `${fileName}: ${lots.length} lots [${lots.join(", ")}]`,
count: lots.length
};
}
-
Save, then copy
lot-a.txtandlot-e.txt.The console shows
lot-a.txt: 2 lots [LOT-01, LOT-02]. Forlot-e.txt, it showsFailed to execute code with error 'Unexpected line 'Lot 8' in lot-e.txt', followed by the stack trace, and no summary.
When main throws, the task logs the reason, emits the error on its Error output, and does not emit Success, so the tasks activated by Success do not run. Link Error to the task that handles the failure, or let an Unhandled Error task report it. Throw an Error object, never a text: the task reports the message of the error, and a text has none.
Return the results as one object at the end. outputs.summary.emit(value) emits while the code runs, but values emitted on one output in quick succession, with no await between them, reach a linked Data Flow task once, with the last value. An output named in the returned object is emitted even when its value is null, so give each value a fallback, as fileName has.
Step 3: Keep a Running Count in the Data Store#
A new instance of the class runs each time the task is activated, and nothing in the code is shared with other tasks, so a class property never reaches the next run. State that must outlive a run goes in the data store of the controller, the same store that the Store and Retrieve tasks use. Replace the return statement of main with these lines:
const total: number = (await this.framework.dataStore.retrieve("lotsSeen", 0)) + lots.length;
await this.framework.dataStore.store("lotsSeen", total, "Temporary");
this.framework.logger.info(`${fileName}: ${lots.length} lots, ${total} since start`);
return {
summary: `${fileName}: ${lots.length} lots [${lots.join(", ")}], ${total} since start`,
count: lots.length
};
-
Save, then copy
lot-a.txt, and thenlot-b.txt.The console shows these messages, each after the timestamp and source of its log line. Each summary of
Log Messageis preceded by the line that the code logs itself:
retrieve returns the default, 0, until a value is stored. Temporary keeps the value in memory, and Persistent writes it to disk so that it survives a restart. The framework object gives the code these services:
| Member | Use |
|---|---|
logger | debug, info, warning, and error, written to the controller log, as Log Message does |
dataStore | store, retrieve, delete, listKeys, and the queue methods, shared with every workflow of the controller |
messageBus | publish, and sendRequest, which waits for the reply |
system and LBOS | call an MES service with an input object built from LBOS |
utils | convertValueToType, sleep, ExecuteWithRetry, and ExecuteWithSystemErrorRetry |
driver | Commands and properties of the driver, only in a workflow associated with a driver |
Table 4: Services of the framework object
ExecuteWithRetry(logger, attempts, sleepBetweenAttempts, code) runs code again after any error, up to attempts times, waiting sleepBetweenAttempts milliseconds between attempts. ExecuteWithSystemErrorRetry takes the same arguments and retries only on MES errors that are known to be transient, such as a deadlock or an object changed by another user. Both return what code returns and throw the last error when no attempt succeeds. For example, to call MES with an input object input:
const output = await this.framework.utils.ExecuteWithSystemErrorRetry(
this.framework.logger, 3, 1000, async () => await this.framework.system.call(input));
Prefer MES tasks, such as Execute Service Call, to calls from code, and the equipment tasks to framework.driver: what the code sends is invisible to a reader of the workflow. See Connect IoT - Talking to MES Tutorial.
Step 4: Test the Code Before You Deploy It#
-
Copy
lot-c.txt,lot-d.txt, andlot-e.txt, one at a time, and compare the console with the expected result:File Expected result lot-c.txt2 lots [LOT-05, LOT-06]: the loop ends with the text.lot-d.txt0 lots []: the empty text gives no lot.lot-e.txtFailed to execute code with error 'Unexpected line 'Lot 8' in lot-e.txt', no summary, and no change to the running total.Table 5: Expected results of the edge cases
-
On the General tab of the
Codetask, set Expiration Time to1, save, and copylot-a.txt.The console shows the warning
Execution Context '<ID>' inputs have expired, which listsfileName, and the summary starts withunknown file.fileNamewaited longer than one millisecond for the read to finish. -
Set Expiration Time back to
10000, the default, and save.
Expiration Time is how long, in milliseconds, the input values of an execution context wait for a new value or an activation before they are dropped. 0 keeps them until the activation. Set it longer than the slowest task between the first value and Activate.
Before you deploy the code to a production controller, also review what the sandbox does not guard:
- No module can be loaded: beyond the JavaScript built-ins, the code reaches only what
frameworkoffers, andevalis disabled. - Nothing stops a long run. A loop that does not end blocks the Automation Controller, so bound every loop.
- The code cannot subscribe to anything. It runs only when
Activatereceives a value, so react to events with trigger tasks. - The controller decodes the stored code as ASCII. Write any other character in the code as an escape, such as
"\u00B0"for the degree sign. - A task with no code, or with code the controller cannot load, logs
No code definedorFailed to compile scriptwhen the workflow starts, and every activation then fails withNo code defined.
Info
To inspect the values of a run with breakpoints, see Debug Automation Controller. To run the same files as an automated test, use the Test Orchestrator File-Raw simulator ↗.
Validate the Result#
- Empty
C:/Lots/Incoming, and note the running total of the last summary in the console. - Copy
lot-a.txt,lot-b.txt,lot-c.txt, andlot-d.txt, one at a time. The console shows one summary per file, with2,8,2, and0lots, and the running total grows by12. - Copy
lot-e.txt. The console shows the error of Step 2, and the running total does not change.
What You Learned#
- Code comes last, after the tasks, converters, and expressions that already do the step.
- The class of the template is the contract: the default export receives
framework, andmainreturns one object whose keys name the declared outputs. - Inside the code you use the declared names, and the task stores its ports with the suffixes
InandOut. - State that outlives a run goes in the data store, and a failure is an
Errorthrown frommain. - Expiration Time decides how long inputs wait for
Activate, and the sandbox does not stop a long run.
Next Steps#
When the same code appears in a second Code task, keeps state of its own, needs to react to events, or is needed by a second controller, move it into an Automation Tasks Library, where it can have automatic tests. See Automation Tasks Library and the task customization guide. To recognize the designs that fail in production, continue to Workflow Anti-Patterns and Troubleshooting by Symptom ↗.
