# Actions

Each action has this basic model. An action receives an input message which depend on the action hook and the previous action in case of an action pipeline. After execution of the action it will output a message which could be altered depending on the action type.

{
"type": "",        // Type of the action.
"message": {       // (Optional) To be merged with the input message.
"topic": "",   // (Optional) Can be a string or null.
"payload": {}  // (Optional) Can be any number, string, object or array.
}
}

Pipeline can consist of actions with type:

• action: Refers to another action to be executed.

• collect: Collect data from a widget.

• consoleLog: Write to the browser’s console log.

• convert: Converts data to and from JSON, Base64.

• copy: Copy to clipboard.

• delegate: delegate the execution context of an action pipeline.

• gettime: Converts relative, ISO UTC and milliseconds since Epoch timestamps.

• function: Advanced Endpoint call to the system.

• modify: Change the model of a widget.

• openLink: Opens a URL in the browser.

• passthrough: Passes the input message to the next action with the option to merge a message and/or payload.

• prompt: Show a dialog.

• refresh: Refresh a widget.

• send: Send data to another widget.

• subscribe: Subscribe to data changes in the system.

• switch: Execute different actions based on conditions.

• transform: Transform the data using MongoDB’s Aggregation Pipeline logic.

• wait: Adds a delay before executing the next action.

• write: Writes a value to an object.

Note: Features marked with (*) are not supported yet.

## Supported Action Types

### Action

Invoke a named action defined in the widget’s own actions collection or in the actions collection at compilation level. Actions defined at the widget level take precedence over those at compilation level. If a widget refers to a named action which exists in his own model and at compilation level, the one in the widget collection will be executed.

{
"type": "action",
"name": "NAME OF THE ACTION"
}

Refer to the write-example-01 compilation to see how named actions are defined and used.

### Collect

Collect data from a widget. The specific data retrieved dependents on the source widget. In general, collect returns the same payload data provided to action pipelines defined directly on the referenced widget. Refer to the widget specific documentation for more details. The collected information is assigned to the provided key name of the message payload. If a key is not specified the whole payload will be overwritten with the collected data.

{
"type": "collect",
"from": "Place the ID of the widget here",
"key": "collectedData"
}

the from field can be set to "self". This allows the pipeline to fetch data from the widget that initiated the actions.

This might seem like an odd thing to do since the message at the beginning of a pipeline will be initialized by the source widget, but as the execution progresses this information may be overwritten. Using the collect action on "self" provides a way to get back to the original content.

### Console Log

Writes the input message payload to the browser’s console log.

{
"type": "consoleLog"
}

Add context by providing a tag.

{
"type": "consoleLog",
"tag": "fetch response"
}

Example to set a fixed text message on the payload:

{
"type": "consoleLog",
"tag": "fetch response",
"message": {
"payload": "This is just a message"
}
}

Example to merge the input message payload with other key-value data.

{
"type": "consoleLog",
"tag": "fetch response",
"message": {
"otherText": "This is just some other text message"
}
}
}

### Convert

Converts the payload to a specified format. Supported formats are:

• json

• base64

{
"type": "convert",
"encode": "json"
}

{
"type": "convert",
"decode": "json"
}

### Copy

Copies the payload to the clipboard.

{
"type": "copy"
}

### Delegate

Using tabs widgets, compilations can be created that contain sub-compilations in each tab. Widgets defined within these cannot directly interact with other widgets in peer level compilations.

The delegate action provides a mechanism to address this constraint.

{
"type": "delegate",
"action":  []  // Action pipeline to be executed in the new context
}

This action is probably easiest to understand by considering an example.

Suppose we have a tabs widget with two tabs, each containing their own text widget, text01 and text02. We want to change the text of text02 when clicking on text01. As a first attempt, we might try something like this:

{
"type": "text",
"text": "Click Me",
"id": "text01",
"actions": {
"onClick": {
"type": "send",
"to": {
"route": [ // This does not work from text01
"tabs01",
"tab02",
"text02"
]
},
"message": {
"payload": "Text on tab 1 was clicked"
}
}
}
}

This fails since, from within the tab01 compilation, there is no widget that resolves to the provided route. The route only makes sense when it is traversed starting at the root compilation.

+ Root (compilation)
|
+--+ tabs01 (widget)
|
+--+ tab01 (compilation)
|  |
|  +--+ text01 (widget)  <-- Action starts here
|
+--+ tab02 (compilation)
|
+--+ text02 (widget)

To get the correct execution context we need to define the action at either the root compilation level, or in the actions section of the tabs01 widget.

Note: The execution context refers to the compilation model from which routes and widget ids are resolved.

A named action can be be defined in the root compilation and invoked from text01. This works since named actions are resolved by searching upwards in the containment hierarchy. In this case the onClick could be defined like so:

{
"type": "text",
"text": "Click Me",
"id": "text01",
"actions": {
"onClick": {
"type": "action",
"name": "modifyWidgetOnSecondTab"
}
}
}

with the named action in the root compilation:

{
"actions": {
"modifyWidgetOnSecondTab": { // this still does not work !
"type": "send",
"to": {
"route": [
"tabs01",
"tab02",
"text02"
]
},
"message": {
"payload": "Text on tab 1 was clicked"
}
}
}
}

However, just declaring the named action in the root compilation is not enough. The execution context is not affected by where the action is declared, unless delegate is used.

{
"actions": {
"modifyWidgetOnSecondTab": {
"type": "delegate",
"action": [
{
"type": "send",
"to": {
"route": [
"tabs01",
"tab02",
"text02"
]
},
"message": {
"payload": "Text on tab 1 was clicked"
}
}
]
}
}
}

In other words, delegate changes the context of the execution pipeline to be at the level where it is defined in the compilation. Using the new context, the pipeline defined in the action property is executed.

When the delegate action returns, the context is restored and any subsequent actions will be executed in the context that was there before.

Note: The context also includes the widget that initiated the pipeline, and is referred to as "self". If the delegate action is declared at compilation level, the self widget will not be set. In the current version, self cannot yet be used in route expressions of delegated actions.

### GetTime

The gettime action can perform two types of functions:

• Convert a relative time expression to the equivalent ISO UTC time string or an Epoch timestamp. By default the output is an ISO string. If the optional asEpoch property is set to true, the output is returned as a number.

• Convert between ISO UTC string and epoch integer

A time relative expression consists of a , indicating *now, optionally followed by an offset expression subtracted from, or if required, added to the current time.

The offset is stated as an integer number followed by a time scale unit. The supported time scale units are:

• ms - millisecond

• s - second

• m - minute

• h - hour

• d - day

• w - week

Here are some examples of relative time expressions:

Expression Description

*

Now

*-5d

5 days ago

*+30m

30 minutes from now

The table below illustrates the conversions performed by the gettime action.

type value type value example results

Relative

string

*-1d

Now minus one day as an ISO UTC string. Unless: asEpoch : true Then the output is the number of milliseconds since January first 1970

ISO UTC

string

2021-04-28T09:44:35.668Z

1619603075668

Milliseconds since Epoch

number

1619603075668

2021-04-28T09:44:35.668Z

The JSON below shows an example of gettime used to convert relative times to absolute ISO UTC strings:

{
"type": "gettime",
"set": [
{
"name": "starttime",
"value": "*-5d"
},
{
"name": "endtime",
"value": "*-1d"
}
]
}

Resulting message:

{
"starttime": "2021-04-24T10:31:04.091Z",
"endtime": "2021-04-28T10:31:04.092Z"
}
}

This example shows how to convert a relative time to its Epoch equivalent:

{
"type": "gettime",
"set": [
{
"name": "starttime",
"value": "*-5d",
"asEpoch" : true
}
]
}

yields this output message:

{
"starttime": 1619260264103
}
}

The action can also take the set instruction from the message payload to do dynamic conversions.

Action message input:

{
"set": [
{
"name": "myTimestamp",
"value": "*-2d"
}
]
}
}

Action:

{
"type": "gettime"       // Notice it does not contain a set field.
}

Result is the message output:

{
"set": [],                      // Since the action result is merged with the message input the set is still present.
"myTimestamp": 1630497600000
}

### Function

{
"type": "function",
"lib": "LIBRARY NAME",
"func": "FUNCTION NAME", // (Optional) Function name in case library is Lua table.
"farg": {},              // (Optional) Function argument.
"ctx": ""                // (Optional) System object path.
}

### Modify

With modify the model of a widget can be changed. If no modification operator is specified a mergeObjects will be performed. This will merge / overwrite the properties from the message payload into the model.

To point to the widget which needs to be modified a id field. The value is normally a string with the widget ID of the designated widget. In case you need to point to a single tab of a Tab widget or a widget which is residing within a tab, you need to provide a route.

{
"type": "modify",
"id": "TextWidget"      // Pointing to a Text widget which has an ID of 'TextWidget`.
}

Using a route to point to a widget within a tab

{
"type": "modify",
"id": {
"route" : [
"MyTabs",       // ID of the Tabs widget.
"Tab01",        // ID of the single tab.
"TextWidget"    // Pointing to a Text widget which has an ID of 'TextWidget` which.
]
}
}

The following modification operators are supported, multiples of which can be specified in the same action. They are evaluated in the order shown. In other words, if multiple modification operators are used, the set modification is done first, followed by unset and so on.

• set: Add field to model or update field.

• unset: Remove field from model.

• removeFromArray: Removes one or more items from an array that matches the provided fields.

• filter: Removes items from an array field based on a condition.

A transform action will be performed under the hood with completeMsgObject set to true. The model field is added to the input message by the modify action before the transform actions starts. It is read from the model of the widget being modified (as designated by the id field).

The message payload, which is passed down from the originating widget or previous action in the pipeline is typically used as the source for setting model fields.

Main signature of the modify action is:

{
"type": "modify",
"id": "TextWidget",
"refresh": true,    // Default is true, if you don't want to let the widget perform a refresh, set it to false.
"debug": false      //  If true, writes the model after the modification to the console log.
}

Merge the widget model with the message payload:

{
"type": "modify",
"id": "TextWidget"
}

Will result in a transform action:

{
"type": "transform",
"completeMsgObject": true,
"aggregateOne": [
{
"$project": { "model": { "$mergeObjects": [
"$model", "$payload"
]
},
"payload": "$payload" } } ] } To modify a specific sub document of the model, make use of the supported modification operators. Example to update the font size: { "type": "modify", "id": "TextWidget", "set": [ { "name": "model.options.style.fontSize", "value": "50px" }, { "name": "model.options.style.fontFamily", "value": "Courier New" } ] } Will result in a transform action: { "type": "transform", "aggregateOne": [ { "$set": {
"model.options.style.fontSize": "50px",
"model.options.style.fontFamily": "Courier New"
}
}
]
}

Example to update the style:

With this example the custom style will only include fontSize.

{
"type": "modify",
"id": "TextWidget",
"set": [
{
"name": "model.options.style",
"value": {
"fontSize": "50px"
}
}
]
}

Will result in a transform action:

{
"type": "transform",
"aggregateOne": [
{
"$set": { "model.options.style": { "fontSize": "50px" } } } ] } Example to remove the style example: This way the widget uses the default style. { "type": "modify", "id": "TextWidget", "unset": ["model.options.style"] } Will result in a transform action: { "type": "transform", "aggregateOne": [ { "$project": {
"model.options.style": 0
}
}
]
}

Example to update an item by index:

This example changes the aggregate of the first pen in a chart. Since the field notation is one-on-one used in the Aggregation Pipeline, the index is zero based.

{
"type": "modify",
"id": "chartWidget",
"set": [
{
"name": "model.chart.pens.0.aggregate",
"value": "AGG_TYPE_AVERAGE"
}
]
}

Will result in a transform action:

{
"type": "transform",
"completeMsgObject": true,
"aggregateOne": [
{
"$set": { "model.chart.pens.0.aggregate": "AGG_TYPE_AVERAGE" } } ] } Example to add an item to the data array of a table widget: { "type": "modify", "id": "fixedTableWidget", "addToArray": [ { "name": "model.data", "value": { "column1": 1, "column2": 2 } } ] } Will result in a transform action: { "type": "transform", "completeMsgObject": true, "aggregateOne": [ { "$set": {
"model.data": {
"$concatArrays": [ { "$cond": [
{
"$isArray": [ "$model.data"
]
},
"$model.data", [] ] }, [ { "column1": 1, "column2": 2 } ] ] } } } ] } Example to remove an item from an array by index: Removes the second row from a fixed data table. { "type": "modify", "id": "fixedTableWidget", "removeFromArray": [ { "name": "model.data", "idx": 2 } ] } Will result in a transform action: { "type": "transform", "completeMsgObject": true, "aggregateOne": [ { "$project": {
"model.data.1": 0
}
}
]
}

Example to remove an item from an array based on a match expression:

Removes the rows from a fixed data table of which name contains value Inside Temperature and value 26.

{
"type": "modify",
"id": "fixedTableWidget",
"removeFromArray": [
{
"name": "model.data",
"item": {
"name": "Inside Temperature",
"value": 26
}
}
]
}

Alternative structure:

{
"type": "modify",
"id": "fixedTableWidget",
"removeFromArray": [
{
"name": "model.data",
"item": [
{
"name": "name",
"value": "Inside Temperature"
},
{
"name": "value",
"value": 26
}
]
}
]
}

Will result in a transform action:

{
"type": "transform",
"completeMsgObject": true,
"aggregateOne": [
{
"$set": { "model.data": { "$filter": {
"input": "$model.data", "as": "item", "cond": { "$and": [
{
"$ne": [ "$$item.name", "Inside Temperature" ] }, { "ne": [ "$$item.value", 26 ] } ] } } } } } ] } Example to filter items from an array: Filters the rows from a fixed data table of which the column value is greater than or equal to 20. The condition is an Aggregation Pipeline filter condition. Within the condition $$item is reserved for referencing an item in the array. { "type": "modify", "id": "fixedTableWidget", "filter": [ { "name": "model.data", "condition": { "gte" : [ "$$item.value", 20 ] } } ] } Will result in a transform action: { "type": "transform", "completeMsgObject": true, "aggregateOne": [ { "$set": {
"model.data": {
"$filter": { "input": "$model.data",
"as": "item",
"cond": {
"$gte" : [ "$$item.value", 20 ] } } } } } ] } Note: Due to how the filter pipeline is implemented in the external library, the condition expression can only refer to fields contained within$$item. What this means is that external variables are not visible inside the condition expression. ### Notify Displays a notification on the top right of WebStudio. { "type": "notify", "title": "Copied", "text": "📋 Copied to Clipboard", "duration": 2500, "transition": "slide" } title is optional and is by default set to the title defined in the widget captionBar. In case the title is not defined in the caption bar the name or id of the widget will be shown. duration is optional and by default 3000. transition is optional with a default value of slide. • slide Smooth sliding of notification • bounce Bounce in of the notification • zoom Zoom in and out • flip Flips the notification Opens a hypermedia link. { "type": "openLink", "url": "https://www.lipsum.com", "target": "_blank" } target is optional and by default _blank. • _self Opens the document in the same window/tab as it was clicked. • _blank Opens the document in a new window or tab. ### Passthrough The input message will be passed through to the next action with the option to merge with a defined message. { "type": "passthrough", "message": { "someField": "This field will be merged besides input message topic and payload", "payload": { "attrib": "This field will be merged within the input message payload" } } } ### Prompt (Dialog) This action causes a popup dialog (prompt) to be shown. Its content is a widget model, contained within the payload of the message. Example to prompt a text widget. { "type": "prompt", "width": "500px", "height": "500px", "message": { "payload": { "type": "text", "text": "Hello world" } } } • content: Can be a single widget or a complete compilation. ### Read Reads the dynamic value of an object or the value of an object property by executing a Read endpoint request. Example read object’s dynamic value. { "type": "read", "path": "/System/Core/Examples/Demo Data/Process Data/FC4711" } Example read object’s property value. { "type": "read", "path": "/System/Core/Examples/Demo Data/Process Data/FC4711.OPCEngUnit" } Example to read a Lua KPI table object. { "type": "read", "path": "/System/Core/Examples/Demo Data/Process Data/FC4711", "opt": { "STARTTIME": "2020-10-13T00:00:00.000Z", "ENDTIME": "2020-10-14T00:00:00.000Z", "TIMESTAMP": "1602633600" } } Example of the query mechanism support by the Read Web API endpoint. { "type": "read", "path": "/System/Core/Examples/Demo Data/Process Data/DC4711", "opt": { "q": "COREPATH" } } • opt: optional field, supported arguments are described in the Read endpoint section in the Web API documentation. ### Read-write Can be used for widgets which support reading and writing. { "dataSource": { "type": "read-write", "path": "/System/Core/Examples/Tables/Example01" } } ### Refresh Refresh a widget. No message will be send to the target widget. In case you want to send a message to a widget, use a send action with the message topic set to refresh. { "type": "refresh", "id": "Place the ID of the widget here" } The to field can have the value self in case the action pipeline needs to refresh its own widget. ### Send Widgets data exchange Sending message from one widget to another can be done using the send action. This action does not change the output message. The output message is the same as the input message. { "type": "send", "to": "Place the ID of the widget here" } The value of the to field can be set to self in case the pipeline needs to send data to its own widget. The update behavior differs per widget. Using a route to point to a widget within a tab { "type": "send", "to": { "route" : [ "MyTabs", // ID of the Tabs widget. "Tab01", // ID of the single tab. "TextWidget" // Pointing to a Text widget which has an ID of 'TextWidget` which. ] } } Supported topics: • refresh: the recipient widget will perform a refresh. (default) • update: the receiving widget will perform an update. This will bypass the data source action. #### Refresh Topic The recipient widget will perform a refresh. It will execute the data source action (pipeline) with the provided message payload. The refresh life cycle will be performed including a fetch if a data source is present. Example to send a message to another component to refresh itself: { "type" : "send", "to" : "Place the ID of the widget here", "message": { "topic": "refresh" // Can be omitted because it is default. "payload": {} // Can be any type of value. Typically an object is ued. } } #### Update Topic The recipient widget will perform an update. It only updates its known properties with the provided message payload. The update life cycle will be performed. { "type" : "send", "to" : "Place the ID of the widget here", "message": { "topic": "update", "payload": {} // Can be any type of value. Typically an object is ued. } } ### Subscribe Subscribe to object data changes in the system. Typically used in dataSource configurations. If necessary, use the willUpdate action hook to transform the data. { "type": "subscribe", "path": "/System/Core/Examples/Variable" } ### Switch Execute actions based on rules. A rule will be checked by performing a queryOne transformation. In case the result of the queryOne transformation is something other than null the action(s) defined in the case statement will be executed. If one rule matches, its action will be executed and further testing of the subsequent rules will be stopped. In case checkAll is set to true, the 'initial' input message of the switch will be passed to each action pipeline of the matched rules. The output message of the last executed action pipeline, will be the output message of this switch action. When no rule matches, the output of the switch action is the same as the input. In order to have a default action in case none of the rules match, add a extra rule at the end of the case array with an empty match condition. { "type": "switch", "checkAll": false, "case": [ { "match" : { "temp": 10 }, "action" : { // Can be a single action or action pipeline. "type": "action", "name": "doSomething" } }, { "match" : { "temp": { "$gte": 20
}
},
"action" : [ // Can be a single action or action pipeline.
{
"type": "action",
"name": "doSomethingFirst"
},
{
"type": "action",
"name": "doSomethingExtra"
}
]
},
{
"match" : {}, // Default action
"action" : [] // Can be a single action or action pipeline.
}
]
}

### Transform

Transformation of data can be performed by means of the MongoDB Aggregation framework. The input data is normally the value of the payload field of the input message. In case the whole message object needs to be available to the transform logic you can set completeMsgObject to true.

The aggregation pipeline often returns an array of one or more elements. In most cases, pipeline actions which ingest the output of the transformation are however looking for a single object. As a convenience the WebStudio specific aggregateOne options can be used instead of aggregate. It return the first element from the resulting transformation.

Besides aggregate and aggregateOne, query and queryOne can also be used in scenarios where filtering is required. As expected queryOne returns the first matching object.

MongoDB Documentation:

#### Transform Example 01

This example is to transform the data to an object with a different key.

{
"name": "Company A",
"location": "Eindhoven"
}
• Transform logic:

{
"type": "transform",
"aggregateOne": [
{
"$project": { "company": "$name"
}
}
]
}

{
"company": "Company A"
}

Example to search for the object with 'location' value 'Cologne':

[
{
"name": "Company A",
"location": "Cologne"
},
{
"name": "Company B",
"location": "Eindhoven"
}
]
• Transform logic:

{
"type": "transform",
"aggregate": [
{
"\$match": {
"location": "Cologne"
}
}
]
}

[
{
"name": "Company A",
"location": "Cologne"
}
]

Note: Even though only one element was matched, the returned massage payload is still an array. If aggregateOne were used instead of aggregate, the output would look like this:

{
"name": "Company A",
"location": "Cologne"
}

#### Transform Example 02

This example uses a query to search for the object with 'location' value of 'Cologne'.

[
{
"name": "Company A",
"location": "Cologne"
},
{
"name": "Company B",
"location": "Eindhoven"
},
{
"name": "Company C",
"location": "Cologne"
}
]
• Transform logic:

{
"type": "transform",
"query": {
"location": "Cologne"
}
}

[
{
"name": "Company A",
"location": "Cologne"
},
{
"name": "Company C",
"location": "Cologne"
}
]

#### Transform Example 03

This example uses queryOne to search for the object with 'location' value 'Cologne'. Like aggregateOne, it returns only the first match.

[
{
"name": "Company A",
"location": "Cologne"
},
{
"name": "Company B",
"location": "Eindhoven"
},
{
"name": "Company C",
"location": "Cologne"
}
]
• Transform logic:

{
"type": "transform",
"queryOne": {
"location": "Cologne"
}
}

{
"name": "Company A",
"location": "Cologne"
}

### Wait

Wait before the next actions will be executed. duration is in milliseconds.

{
"type": "wait",
"duration": 1000
}

### Write

Writes a value to an object in the system. The write action inspects the message payload passed to it, looking for variables v, q and t which it uses to update the object identified by the path.

{
"type": "write",
"path": "/System/Core/Examples/Variable"
}

#### Input Message

Example:

{
"v": 79,
"t": "2021-08-10T08:34:13.000Z",
"q": 0
}
}

The value parameter v will be a number for most tags and is the only mandatory variable. It may be specified explicitly as shown above, or can be set as the value of the payload itself as shown below. In the latter case, t and q cannot be set

// Value supplied as the payload
{
}

If the timestamp field t is omitted, the current time is assumed by the core. If present, t must be expressed as an ISO UTC string.

It would be rare to supply a quality value q explicitly, but the option is available. The parameter can be omitted in most cases in which case 0 (GOOD) is assumed.

#### Return Message

The write action return message, which can be passed to a subsequent action in a pipeline, is dependent on which parameters were supplied to it.

For an input message containing only a value:

// Input message
{
"v": 79
}
}

the return message looks like this:

// Output message
{
}

On the other hand, if the value and either the time-stamps or the quality parameters are supplied:

// Input message
{
"v": 79,
"q": 0
}
}

the output will contain all vqt values returned from the write call made to the backend:

// Output message
{