--- render_macros: false pdfexport: true alias: tutorials-connectiot-writing-a-code-task timetoread: true tutorial: full module: Connect IoT (Equipment Integration) description: "Write a Code task that turns the text of a lot file into a summary, keeps a running count in the data store, and test it with edge-case files before you deploy it" --- # 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 | [[user-guide-automation-task-core-expressionevaluatorgeneric]] | | Pick values out of a JSON object or reshape it | [[user-guide-automation-task-core-jsonata]] | | Change one value on its way along a Data Flow link | A converter. See [[user-guide-automation-converters-index]]. | | Compute the value of one Control Flow row | An expression. See [[user-guide-automation-controller-workflow-controlflow-expressions]]. | | Branch, loop, or retry | The Control Flow tasks. See [[tutorials-connectiot-control-flow-in-depth]]. | | 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 [[tutorials-connectiot-how-values-travel]] and [[tutorials-connectiot-control-flow-in-depth]] 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 [[user-guide-automation-task-core-code]]. ## Before You Start - Set up the controller, the watched folder `C:/Lots/Incoming`, and the files `lot-a.txt` and `lot-b.txt` as in [[tutorials-connectiot-control-flow-in-depth#before-you-start]]. - 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 the `File Handling` page 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.txt` | `LOT-05`, `LOT-06` | A file without `END` | | `lot-d.txt` | Nothing | An empty file | | `lot-e.txt` | `LOT-07`, `Lot 8`, `END` | A 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 1. On the **Code** tab of the `Code` task, keep the class of the template and replace the body of `main`: ```typescript 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 { const text: string = String(inputs.content ?? ""); return { summary: `${inputs.fileName}: ${text.length} characters` }; } } ``` ![Lot Summary page linked in sequence](images/connectiot_wct_step1.png) 2. Save the **Automation Controller**, then copy `lot-a.txt` into `C:/Lots/Incoming`. The console shows `lot-a.txt: 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: ```typescript public async main(inputs: any, outputs: any): Promise { 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 }; } ``` 1. Save, then copy `lot-a.txt` and `lot-e.txt`. The console shows `lot-a.txt: 2 lots [LOT-01, LOT-02]`. For `lot-e.txt`, it shows `Failed 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: ```typescript 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 }; ``` 1. Save, then copy `lot-a.txt`, and then `lot-b.txt`. The console shows these messages, each after the timestamp and source of its log line. Each summary of `Log Message` is preceded by the line that the code logs itself: ```text lot-a.txt: 2 lots, 2 since start lot-a.txt: 2 lots [LOT-01, LOT-02], 2 since start lot-b.txt: 8 lots, 10 since start lot-b.txt: 8 lots [LOT-01, LOT-02, LOT-03, LOT-04, LOT-05, LOT-06, LOT-07, LOT-08], 10 since start ``` `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`: ```typescript 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 [[tutorials-connectiot-talking-to-mes]]. ### Step 4: Test the Code Before You Deploy It 1. Copy `lot-c.txt`, `lot-d.txt`, and `lot-e.txt`, one at a time, and compare the console with the expected result: | File | Expected result | |---|---| | `lot-c.txt` | `2 lots [LOT-05, LOT-06]`: the loop ends with the text. | | `lot-d.txt` | `0 lots []`: the empty text gives no lot. | | `lot-e.txt` | `Failed 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 2. On the **General** tab of the `Code` task, set **Expiration Time** to `1`, save, and copy `lot-a.txt`. The console shows the warning `Execution Context '' inputs have expired`, which lists `fileName`, and the summary starts with `unknown file`. `fileName` waited longer than one millisecond for the read to finish. 3. 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 `framework` offers, and `eval` is 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 `Activate` receives 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 defined` or `Failed to compile script` when the workflow starts, and every activation then fails with `No code defined`. !!! info To inspect the values of a run with breakpoints, see [[user-guide-debug-automation-controller]]. To run the same files as an automated test, use the Test Orchestrator [File-Raw simulator](https://developer.criticalmanufacturing.com/explore/guides/customizations/automation/how-tos/test-orchestrator/plugins/simulators/FileRaw/). ### Validate the Result 1. Empty `C:/Lots/Incoming`, and note the running total of the last summary in the console. 2. Copy `lot-a.txt`, `lot-b.txt`, `lot-c.txt`, and `lot-d.txt`, one at a time. The console shows one summary per file, with `2`, `8`, `2`, and `0` lots, and the running total grows by `12`. 3. 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`, and `main` returns 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 `In` and `Out`. - State that outlives a run goes in the data store, and a failure is an `Error` thrown from `main`. - **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 [[user-guide-automation-tasks-library-index]] and the [task customization guide](https://developer.criticalmanufacturing.com/explore/guides/customizations/automation/customization-components/tasks/customization_task/). To recognize the designs that fail in production, continue to [[user-guide-automation-workflow-antipatterns]] and [[user-guide-automation-troubleshooting-symptoms]].