Rule and Procedure Reference Guide

Overview

Vantiq uses rules and procedures to filter data streams, validate object instances, identify situations, evaluate recommendations and implement responses.

Rules and procedures are constructed using a common set of declarative constructs, simplifying the learning curve for building IoT automation systems, and making it very simple to re-allocate business logic among procedures and rules. The declarative notation used in defining rules and components is a SQL and Javascript derived DSL called VAIL, the Vantiq Automation and Integration Language. In order to make the VAIL more familiar and easier to learn, the DSL’s control structures and built-in types are loosely modeled on Javascript, while data definition and data manipulation declarations are loosely modeled on SQL.

A VAIL declaration contains:

In general, rules and procedures operate on the state of the automation system and the events that signal changes to the automation state. The automation state consists of information from external systems, applications and sensors as well as any state information configured with the automation system. For example:

The system is optimized to ingest streaming data, identify important situations by inspecting the streaming data and the current context represented by the automation state and, finally, recommend responses and implement responses to “close the loop” and complete the automation cycle. Two key concepts in this process are situations and recommendations.

This document is a reference manual for VAIL, explaining how to use VAIL to most effectively detect important situations and respond to the situations with recommended responses.

Declaration

Rules and procedures are specified by submitting a VAIL declaration of the rule or procedure. VAIL syntax and semantics are described in VAIL Syntax and Semantics. The VAIL statements determine the behavior of a rule. The body of the rule or procedure consists of a sequence of VAIL statements from the following statement types:

REST Interface for Rules

The Vantiq server may have an environment specific domain name. In this document the default development server, https://dev.vantiq.com, is the server used in statement syntax blocks and examples.

Register

A new or updated rule or component may be submitted using the register command:

POST https://dev.vantiq.com/api/v1/resources/rules[?active=false]
<rule definition>

If active is set to false, the rule is initially placed in the inactive state. By default, new or updated rule definitions are placed in the active state.

See VAIL Syntax and Semantics for a detailed description of a rule definition.

Delete

A rule or component may be deleted by issuing the delete command:

DELETE https://dev.vantiq.com/api/v1/resources/rules/<ruleName>

Delete the named rule or component.

Change Rule Activation State

A rule or component can be set to the active state via a REST request:

PATCH https://dev.vantiq.com/api/v1/resources/rules/<ruleName>

with the JSON message body:

[
    {
        "op": "replace",
        "path": "/active",
        "value": true
    }
]

A rule can be set to the inactive state via a REST request with the JSON body:

[
    {
        "op": "replace",
        "path": "/active",
        "value": false
    }
]

Distributed Processing

Vantiq supports distributed execution of VAIL statements which will cause them to be executed by a set of remote Vantiq servers. The statements which support distributed processing are:

Remotely executable statements all support the PROCESSED BY clause, which describes any remote processing for the statement. The PROCESSED BY clause has two distinct forms, depending on whether the statement results should be returned as an array or processed as a sequence. The sequence form of the clause (which is preferred) is:

[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]

For example:

INSERT MyType(name: "myName") AS result PROCESSED BY ars_properties.region == "West"
CONTEXT TO resultCtx {
    log.info("Inserted object with id {} on node {}", [result._id, resultCtx.node.name])
} ON REMOTE ERROR (error) {
    log.warn("Failed to insert on node {} due to error: {}", [error.node.name, error.error.message])
}

The array form of PROCESSED BY, which has been deprecated and will eventually be removed, is:

PROCESSED BY <queryConstraint> [STATUS TO <statusVar>]

For example:

var results = INSERT MyType(name: "myName") PROCESSED BY ars_properties.region == "West" STATUS TO status
FOR i in range(0,size(results)) {
    IF (status[i].status == "SUCCESS") {
        log.info("Inserted object with id {} on node {}", [results[i]._id, status[i].node.name])
    } ELSE {
        log.warn("Failed to insert on node {} due to error: {}", [status[i].node.name, status[i].error.message])
    }
}

Selecting Remote Targets

The Vantiq nodes resource represents connections to remote installations of the Vantiq server on which remote statements may be executed. Nodes are defined using the CREATE NODE statement.

The queryConstraint defined as part of the PROCESSED BY clause selects the node instances where the statement will be executed. The queryContraint is a logical expression of the same form used by the query condition in a SELECT statement which is applied to the nodes resource. Any instances of nodes that satisfy the constraint will be asked to execute the statement and return the results to the local node. In the examples above the statements will execute on all nodes where the region key in ars_properties has been set to "West".

The query constraint also recognizes the keyword ALL which causes the statement to be executed by all remote nodes.

Bounding Request Time

The WITHIN sub-clause can be used to limit the amount of time that the system will wait for a remote node to produce its results. The timeInterval is specified as an interval constant (for example “1 minute”). Any node that does not produce a result within the specified amount of time will instead produce a “timeout” error (which is then ignored or processed just as any other remote error).

Processing Results

Depending on the form of PROCESSED BY used the statement results will either be returned as a sequence or an array.

Sequence Results

Processing results as a sequence allows for more efficient handling of larger result sets and for incremental processing of any errors that occur during remote processing. For this reason, it is the preferred approach. In this form the remote statement produces a sequence of results which are processed by the supplied processing block. For all statements other than SELECT, the sequence will contain one result for each node on which the statement was executed. For SELECT the results from all nodes are merged into a single sequence which is then processed. The associated processing block is invoked once for each item in the sequence with the variable defined in the “AS” clause containing that item (the AS clause is optional because SELECT statements already have a way of declaring the sequence variable).

Processing of the sequence will continue until it has been exhausted, unless the processing block include an UNTIL sub-clause. This sub-clause specifies a logical expression which is evaluated after each invocation of the processing or error handling blocks. If the expression evaluates to true then processing of the sequence will be immediately terminated. See the FOR statement for examples of UNTIL processing.

The CONTEXT TO sub-clause is used to specify a variable which will hold the request status for the sequence item currently being processed. The status is an object which has the following properties:

For example, here is the context for an item produced by “node1”.

    { 
        "node": { "name" : "node1", ... }, 
        "status": "SUCCESS" 
    }

When errors occur in the execution of the request and/or the production of the resulting sequence they will be ignored, unless the PROCESSED BY clause includes an error handler defined via the ON REMOTE ERROR sub-clause. When the error handler is defined it will be invoked for any such error and the declared error variable will be populated with a status object with the following properties:

For example, here is the error status for a request to “node3” which failed due to an authentication problem.

    { 
        "node": { "name" : "node3", ... }, 
        "status": "FAILURE",
        "error":  {
            "code": "io.vantiq.security.authentication.failed", 
            "message": "Request timed out."
        }
    },

Array Results

The array form of PROCESSED BY is provided to maintain compatibility with prior Vantiq releases. However, it is currently deprecated and will be removed entirely in a future Vantiq release. For all statements other than SELECT, results are placed in an array such that there is one element in the array for each node on which the statement was executed. For SELECT the results will be merged into a single array using the standard SELECT query result semantics.

The array form supports obtaining the overall request status via the STATUS TO sub-clause. This sub-clause specifies the name of a variable that will be populated with an array with one status object for each Vantiq node that participated in the distributed request. The structure of these status objects is the same as previously described above for the context and error variables. For example:

[
    { 
        "node": { "name" : "node1", ... }, 
        "status": "SUCCESS" 
    },
    { 
        "node": { "name" : "node2", ... }, 
        "status": "SUCCESS" 
    },
    { 
        "node": { "name" : "node3", ... }, 
        "status": "FAILURE",
        "error":  {
            "code": "io.vantiq.federation.request.timeout", 
            "message": "Request timed out."
        }
    },
    { 
        "node": { "name" : "node4", ... }, 
        "status": "FAILURE",
        "error": {
            "code": "io.vantiq.security.authentication.failed", 
            "message": "Invalid credentials."
        }
    }
]

In this example, the request was sent to four servers, two of which responded successfully, one of which failed because the user’s credentials were inadequate, and one of which failed to respond and is, presumably, offline at the present time.

VAIL Syntax and Semantics

Rules and procedures are at the heart of the IoT automation services, since rules and procedures are used to filter IoT data, identify situations and make recommendations.

PROCEDURE

A procedure definition consists of an optional service name, the procedure name, formal parameters and a set of statements representing the body of the procedure. The syntax of the PROCEDURE statement:

PROCEDURE [<serviceName>.]<name>(<parameters>)[WITH properties = <procedureProperties>]

The PROCEDURE statement is followed by a sequence of statements that define the behavior of the procedure.

For example:

PROCEDURE changeSalary(empName, newSalary)

UPDATE Employee (salary: newSalary) WHERE name == empName

This simple procedure accepts two parameters, an employee name and a new salary for the employee. When invoked it updates the employee assigning the new salary to the employee. Of course, a more realistic example would be far more complicated dealing with exceptional conditions such as the employee does not exist or a salary is specified that is outside the standard salary range for the employee’s position.

Service Name

The declaration of a procedure may include an optional service name. If a service name is provided then the procedure will be created as part of the declared service (along with any other procedures that are similarly defined). The service name becomes part of the procedure’s identifier and must also be provided when invoking the procedure in VAIL code. The system will automatically create services as they are declared by procedures so there is no need to create them in advance.

For example:

PROCEDURE EmployeeUtils.changeSalary(empName, newSalary)

UPDATE Employee (salary: newSalary) WHERE name == empName

This declares procedure named “changeSalary” which is part of the service “EmployeeUtils”. To call this procedure one would write VAIL code like this:

var employee = <... create or select employee instance ...>
EmployeeUtils.changeSalary(employee, 50000)

Name

The name of a procedure may consist of alphanumeric characters and the underscore character. The procedure name must start with an alphabetic or underscore character. It may not start with a numeric character. In addition, all names that start with “ars_” are reserved for system use.

Parameter Declarations

VAIL supports both untyped and typed parameter declarations.

With untyped parameters declarations each formal parameter is declared with a name. VAIL uses “duck typing” so no formal type declaration is required. Rather, at runtime the Vantiq system will confirm that the request operations can be performed and produce an error if they cannot. The previous example uses untyped parameter declarations.

With typed parameter declarations each formal parameter is declared with a name, a type and a set of modifiers that describe more detailed information about the parameter. The previous example might be recast using typed parameters as follows:

PROCEDURE changeSalary(empName String, newSalary Integer)

UPDATE Employee (salary: newSalary) WHERE name == empName

This declares the empName parameter must be a String value and the newSalary parameter must be an Integer value.

The types supported in procedure parameter declarations are the same types supported in type declarations and have the same semantics:

If the modifier ARRAY is appended to the parameter declaration the parameter is assumed to contain an array of values of the declared type. For example,

PROCEDURE changeSalary(empName String ARRAY, newSalary Integer ARRAY)

FOR (i in range (0, empName.size(), 1)) { 
    UPDATE Employee (salary: newSalary[i]) WHERE name == empName[i]
}

defines a procedure which accepts an array of names and salaries and updates all the employees in the two arrays. This example assumes the two arrays are the same size. If the newSalary array is shorter than the empName array a runtime error will be produced.

Sequences are not currently supported as procedure parameters.

If the modifier DESCRIPTION is appended to the parameter declaration followed by a valid string constant, the string is assumed to be a description of the parameter that will be used for documentation purposes. For example,

PROCEDURE changeSalary(
    empName String ARRAY DESCRIPTION "An array of employee names.",
    newSalary Integer ARRAY DESCRIPTION "An array of salaries. Each salary defines the new salary for the employee name at the corresponding position in empName.")

FOR (i in range (0, empName.size(), 1)) { 
    UPDATE Employee (salary: newSalary[i]) WHERE name == empName[i]
}

The DESCRIPTIONS will be displayed in the developer portal whenever values are solicited for an invocation of the procedure.

Procedures implement call by value semantics. That is, if a parameter value is an array or an object, a copy of the array or object is made and the copy is passed as the actual value of the parameter. This implies that changes to an array or an object made within the procedure will NOT be seen by the caller unless the updated values are explicitly returned to the caller as part of the procedure’s return value. The call by value semantics make distributed procedure invocations behave identically to local procedure invocations enabling transparent distributed processing.

Procedure invocations (described elsewhere in this document) must supply the procedure name and a set of named parameters.

Return Value

The procedure also produces a result. By default this will be the value of the last statement executed. In the previous example, the last value returned is an object containing the properties updated by the UPDATE statement - the last statement in the body of the procedure. This default behavior can be altered with the use of the RETURN statement (which must itself be the last statement of the procedure).

It is legal to return a sequence as the result of a procedure. For example,

PROCEDURE employeesWithMinSalary(minSalary)

FILTER (emp in SELECT FROM Employee WHERE salary >= minSalary) { true }

Defines a procedure which returns a sequence containing all employees with the given minimum salary.

It is also legal to return “embedded” sequences (a sequence contained in an object or array), though such a procedure could not be invoked remotely.

Procedure Properties

The WITH clause sets properties that can be used to select the procedure in a dynamic execute statement or by the deployment system to determine where in the distributed environment the rule should be deployed.

Hiding System Procedures

Although it is not necessarily recommended, it is possible to define procedures (both with and without services) which have the same name as those defined in the system namespace (with the exception of specific, “built-in” procedures). When this happens the locally defined procedure effectively “hides” the system procedure of the same name. Any references to the procedure will invoke the local version and not the system version. When this occurs, it is still possible to use the system version of the procedure by further qualifying its name using the system. prefix. For example, the following references a system version of our previously defined “changeSalary” procedure:

system.EmployeeUtils.changeSalary()

RULE

The rule statement is the first statement in a rule definition and declares the name, version, any selection criteria associated with the rule and any activation constraint associated with the rule:

RULE <name>[:<version> WHERE <versionSelectionCriteria>] [ACTIVATIONCONSTRAINT <activationConstraint>]

The specifics of name, version, versionSelectionCriteria and activationConstraint are defined in the Name, Versioning and Activation Constraint subsections below.

The RULE statement is followed by a WHEN statement that specifies the triggering condition and a set of statements that specify the behavior of the rule. For example:

RULE myFirstRule
WHEN INSERT OCCURS ON Order AS order

for (mgr IN SELECT ONE * FROM Employee WHERE name == order.salesPersonManager) {
    PUBLISH { message: "There is an order for your review: " + order.orderId }
        TO SOURCE corporateEmail
        USING { recipients: [mgr.emailAddress] }

    if (order.customerName == "myFavoriteCustomer") {
        applyDiscount("10%", order)
    }
}

The rule is named “myFirstRule” as specified in the “RULE clause”.

The triggering condition is an INSERT into the “Order” type as specified in the “WHEN” clause. The order will appear as a variable named order (case sensitive) that is available for use in subsequent rules statements.

The SELECT statement searches for the manager of the salesPerson assigned to the order. If the manager exists, the body of the FOR statement is executed with the Employee object representing the manager assigned to the local variable: mgr.

The first statement in the block emails the manager a request to approve the order.

The second statement states that if the customer’s name is “myFavoriteCustomer” the system will apply a 10% discount to the order by calling the component: applyDiscount.

In the example, and all subsequent examples, language keywords are capitalized for clarity but keywords are case insensitive.

In a nutshell, that is how simple it is to define a rule. Of course, VAIL supports a rich set of expressions used to define far more advanced rules.

Name

The name of a rule may consist of alphanumeric characters and the underscore character. The rule name must start with an alphabetic or underscore character. It may not start with a numeric character. In addition, all names that start with “ars_” are reserved for system use.

Rules may be versioned and multiple versions of a rule may simultaneously be defined. If embedded in a name, the colon (‘:’) character delimits a version number. The character string that precedes the colon is considered the proper name of the rule. The character string that follows the colon is considered the version number. Versions have special semantics as described in the subsection below. Version numbers may contain the characters: alphanumerics, ‘.’, ‘-‘, ‘_’.

An example fully specified name containing both a name and a version identifier: myRule:02.03.044. The name of the rule is myRule. The version of the rule is 02.03.044.

Versioning

A rule may be versioned to simplify the development of new versions of an existing rule. A collection of rules that differ only in their version number are considered to be the same rule and only one rule in the collection will be evaluated when a triggering event occurs.

By default, the most recent rule version is activated in response to an event. However, by incorporating a where constraint on the rule statement, more sophisticated behavior can be achieved. For example, it may be desirable to introduce a new version of a rule but only apply it to a limited set of triggering events - perhaps those associated with a specific set of test data. Evaluation occurs as follows:

For example, given the following two versions of a rule:

RULE myRule:V1
...

RULE myRule:V2 WHERE Employee.salary < 100
...

The following INSERT will trigger the evaluation of myRule:V1

INSERT Employee(name: "Harriet", salary: 1000)

However, the following INSERT will trigger the evaluation of myRule:V2

INSERT Employee(name:"Stanley", salary: 50)

The notion of “youngest” version of a rule is determined by the case insensitive, lexical ordering of the version numbers with the lexically largest value considered the most recent or youngest and the lexically smallest value the oldest.

A lexical ordering for version numbers is illustrated below:

Caution is recommended because there are perverse cases that must be avoided. For example, the following lexical ordering is accurately represented even though considering the ordering of the rules based on their numeric values would result in different ordering:

In order to eliminate such inadvertent surprise orderings, specifying versions in a fixed format with all version identifiers containing the same number of characters is highly recommended.

All versions of a rule MUST be triggered by the same WHEN condition (INSERT, UPDATE, DELETE) on the same type. Only the WHERE clause of the rule statement may vary between versions of the same rule set.

When versions are applied to components, the latest version of the component is always invoked unless the invocation request explicitly requests an earlier version of the component.

Activation Constraint

A rule may declare an activation constraint. The activation constraint is used by the provisioning subsystem to determine if the rule should be installed as active or inactive on a target node. Normally, no activation constraint is specified and the rule is provisioned in its default state on each node on which it is provisioned.

However, there may be times when a rule should be provisioned as active on a select set of nodes, but provisioned as inactive on all other nodes. The activation constraint can be used to accomplish this. The activation constraint is applied to the resource instance for each node on which the rule is provisioned. If the constraint evaluates to true, the rule is installed active on that node. If the constraint evaluates to false, the rule is installed inactive on that node. The activation constraint is expressed as a JSON string containing a query constraint that can be evaluated against the nodes resource..

WHEN and BEFORE

Rules are triggered by the set of events declared in the WHEN or BEFORE statement specified in the rule. When one of the events referenced in the BEFORE or WHEN statement is published, the rule is evaluated by first determining whether the triggering condition is satisfied. If it is, the body of the rule is evaluated. If it is not satisfied, the body of the rule is ignored.

The BEFORE statement declares rules that are evaluated immediately before the declared change is effected in the automation database. For example, an INSERT of a new Customer object would prepare the object for INSERT, then evaluate any insert rules triggered by the statement:

BEFORE INSERT OCCURS ON Customer

The value returned on completion of rule evaluation is the value that will inserted into Customer.

The WHEN statement declares rules that are evaluated after the declared event is published. For example, an INSERT of a new Customer object would prepare the object for INSERT, evaluate any BEFORE rules declared on Customer, INSERT the new Customer object and then schedule any rules with a WHEN clause triggered by the insert:

WHEN INSERT OCCURS ON Customer

WHEN Statement

The WHEN statement is used to declare rules that are evaluated anytime the set of conditions specified in the triggering condition evaluates to true. Since the triggering condition can specify a condition that involves more than one event, the rule is only triggered after all events have been received and evaluated. If the triggering condition is satisfied, the body of the rule is evaluated. If the triggering condition is not satisfied, the rule is dismissed.

Some examples may help clarify the behavior of the WHEN clause:

RULE simpleCondition
WHEN INSERT OCCURS ON Customer
...

This rule is evaluated anytime a new object is inserted into the Customer type:

RULE multipleConditions
WHEN 
  INSERT OCCURS ON Customer 
  BEFORE 
    INSERT OCCURS on Order
    WITHIN 30 seconds 
    CONSTRAIN TO Order.customer == Customer.name
...

This rule is evaluated when an insert occurs on Customer AND, within the next 30 seconds, an Order that belongs to that Customer is inserted. This corresponds to a business case where a new customer has entered their first order immediately after registering. However, rule evaluation first sees the insert event on Customer and realizes it cannot determine whether the triggering condition is satisfied until the corresponding Order event occurs or the 30 second timer expires. Thus, the first term in the triggering condition is evaluated and then evaluation is suspended until the Order event or the timer event occurs. At that time, the triggering condition can be fully evaluated and the rule dispatched according to the result of evaluating the full triggering condition.

Event Binding

All events in Vantiq originate from a specific resource instance and represent an operation which has been performed on that instance. The resources which may generate events are:

Rules are bound to these events using a WHEN clause of the following form:

WHEN EVENT OCCURS ON <eventPath> [AS <alias>] [ WHERE <eventCondition>]

The <eventPath> is a string with the following format: /<resource>/<instance id>[/<operation>]. This classifies each event based on the resource, instance, and operation that it represents. The <operation> is only required for type events since both sources and topics have only one possible operation, “publish”.

The <alias> identifies an alternate name bound to the event object(s). If no alias is provided then the event will be bound to the local variable event. Alias names are discussed in more detail in Aliases. At runtime, the bound variable will contain the event instance being processed. Events have the following properties:

The WHERE clause specifies a query condition that the object triggering the evaluation must satisfy before the rule can be activated. The event condition is a logical expression in the form described in Query Conditions.

The generic event binding provides access to the event meta-data and has the advantage of using a common form, regardless of the event’s classification. VAIL also supports the following, resource specific bindings.

Type Event Binding

The rule is evaluated if one of the declared operations occurs on the specified type. The supported operations are the common database operations:

The structure of the corresponding WHEN clause is as follows:

WHEN [INSERT | UPDATE | DELETE]
OCCURS ON <type> [AS <alias>] [WHERE <eventCondition>]

The <type> identifies the type in the automation schema on which the event must occur. The <type> can also be used to refer to the event object(s) that describe the details of the event.

The <alias> identifies an alternate name bound to the event object(s). Alias names are discussed in more detail in Aliases.

The WHERE clause specifies a query condition that the object triggering the evaluation must satisfy before the rule can be activated. The event condition is a logical expression in the form described in Query Conditions.

As mentioned above, the event object is placed in a variable with the same name as the type on which the event was published. For example:

WHEN INSERT OCCURS ON Sensor WHERE Sensor.reading > 100

The event is published on the type Sensor and the inserted value is found in a variable named Sensor. The value associated with the Sensor variable is the value inserted into the automation database and has the form:

{
    "sensorId": "123",
    "reading": 12.576
}

If the triggering condition involves two events on the same type, the above scheme is not general enough and an AS clause MUST be used to specify the name of the variable in which the new value will be placed. For example, to trigger on the arrival of two stock ticker entries for IBM we might specify a triggering condition such as:

WHEN
  INSERT OCCURS ON Ticker WHERE Ticker.symbol == "IBM"
  BEFORE 
    INSERT OCCURS ON Ticker AS secondQuote WHERE secondQuote.symbol == "IBM"
    WITHIN 1 second 
    CONSTRAIN TO Ticker.price > secondQuote.price

In this example, the first quote found for IBM will be stored in the default variable, Ticker, while the second quote is stored in the user specified variable secondQuote. Of course, the developer may specify explicit names for both events:

WHEN 
  INSERT OCCURS ON Ticker AS firstQuote WHERE firstQuote.symbol == "IBM"
  BEFORE
    INSERT OCCURS ON Ticker AS secondQuote WHERE secondQuote.symbol == "IBM"
    WITHIN 1 second 
    CONSTRAIN TO firstQuote.price > secondQuote.price

Note that property references in the WHERE clause of a WHEN statement MUST be fully qualified with the name of the event that is the source of the value.

An INSERT, UPDATE or DELETE trigger in a WHEN clause causes the rule to be activated AFTER the INSERT, UPDATE or DELETE operation has completed. In order to activate the rule BEFORE the INSERT, UPDATE or DELETE is complete, the BEFORE keyword can be used in place of the WHEN keyword. Using BEFORE will allow the rule to modify the data to be INSERTed or UPDATEd in the database.

See BEFORE Statement for a detailed definition of BEFORE.

Single instance vs. Bulk operations

The previous examples showed the case where the operation affected a single instance of the type. It is also possible to issue operations that operate on multiple instances simultaneously. The main purpose of these bulk operations is to improve performance by avoiding the overhead of having to materialize each affected instance. Therefore the event associated with a bulk operation does not contain the instance data. Instead it contains the following operation metadata:

The system property ars_bulkOperation can be used in a rule constraint to ensure that the rule either does or does not receive events related to bulk operations.

Source Event Binding

A message event is produced when a message is delivered by a source.

The structure of the corresponding WHEN clause:

WHEN MESSAGE ARRIVES FROM <source> [AS <alias>] [WHERE <messageCondition>]

The message being received is available in a variable with the same name as the source. The received message may be assigned to a different variable name using the AS clause to identify the variable. If the message contains a JSON or XML payload, the variable contains the parsed message content. If the payload is not JSON or XML, the variable contains the original data received by the source.

Topic Event Binding

The rule is activated if an event is published on the specified topic or any subtopic of the specified topic. Topics may be data topics such as those handled by the more specialized INSERT, UPDATE and DELETE events above, system timer events or user defined events.

The structure of the corresponding WHEN clause is as follows:

WHEN PUBLISH OCCURS ON <topic> [AS <alias>] [WHERE <topicCondition>]

All topics that start with any of:

are reserved for use by the system. All other topic names are available for use by the user.

User Defined Defined Events

When an event is published on a user defined topic, it is available in the rule evaluation context in the variable ‘event’. The event object may be assigned to a different variable name using the AS <alias> clause. User defined topic events contains two properties of interest:

For example, a user defined event that signals a new voltage reading to process might produce an event object of the form:

{
    "topic": "/VoltageReading/new",
    "newValue": {
        "sensorId": "SampleIdentifier",
        "reading": "12.35"
    }
}

Note that, when subscribing to a user defined topic, the rule must specify the entire topic path and not any of its sub-paths. So to receive the above event the rule would specify the topic “/VoltageReading/new” and not simply “/VoltageReading”.

Query Conditions

Query conditions declare a logical constraint an object must satisfy in order to be included in the evaluation. Such constraints are used in WHEN clauses to more precisely determine when a rule should be activated. Query conditions are also used in SELECT statements to precisely specify the set of objects that should be evaluated.

Query conditions have a SQL-like structure so they are familiar to most database users. However, they do have a somewhat specialized syntax in order to optimize their processing in the context of rules. The query condition consists of one or more comparison clauses connected by the boolean operators AND, OR and NOT.

A comparison clause is defined as follows:

<propertyReference> <comparisonOperator> <expression>

Examples:

product.weight >= 5000

would select only products that weigh more than 5000 (kg)

product.type == "truck"

would select only products that are identified as trucks.

product.type == "truck" and product.weight >= 5000

would select products that are trucks weighing more than 5000 (kg)

The comparison operators supported are:

The <expression> must be a constant, property reference or an expression that resolves to a constant during evaluation. See expressions below for more details.

When used in a WHEN clause property names must be qualified by the name of the type or alias in which the value is held. When used in SELECT, UPDATE or DELETE statements the property names are NOT qualified with the name of the type in WHERE clauses BUT ARE qualified with the name of the type in ON clauses as detailed in SELECT. For SELECT statements the properties can be qualified with alias names. In all unqualified cases, the type is assumed to be the target type of the SELECT, UPDATE or DELETE statement.

Aliases

Sometimes it is necessary to reference more than one set of objects of a given type in a single rule. For example, a set of products may be the target of rule evaluation and part of the evaluation is to compare the targets to another set of products. To disambiguate references to different sets of objects of the same type, aliases are used.

An alias can be declared anywhere a type is referenced (in WHEN and SELECT statements) by suffixing the type declaration with

AS <alias>

Specifically, aliases can be used in WHEN and SELECT statements.

In the hypothetical example above involving two sets of products we might declare the two sets as follows:

SELECT Product AS p WHERE ...
SELECT Product AS comps WHERE ...

In the referencing statements the objects would no longer be referenced by the type name, “Product”, but by the aliases, “p” and “comps”. Once an alias has been declared all references to the corresponding set of objects MUST use the alias and not the type name. For those familiar with SQL, the concept of an alias should be very familiar.

Similar WHEN statement examples:

WHEN INSERT OCCURS ON Employee AS emp WHERE ...

WHEN PUBLISH OCCURS ON "/an/important/user/event" AS important

Alias names must conform to standard VAIL naming conventions and may consist of the alphanumeric characters and the underscore character. The alias name may not begin with a number.

Correlating Events

The triggering condition for a WHEN statement can trigger rule evaluation by correlating more than one event. Correlation occurs when multiple triggering events are specified connected by the temporal operators. A simple example was presented early in the description of the WHEN statement:

RULE multipleConditions
WHEN
  INSERT OCCURS ON Customer 
  BEFORE 
    INSERT OCCURS ON Order
    WITHIN 30 seconds 
    CONSTRAIN TO Order.customer == Customer.name

In this example the rule body is only evaluated if the automation system sees an insert on Customer followed by an insert on Order with the second event occurring within 30 seconds of the first event and both events referencing the same customer object.

The general form of a correlating trigger condition is:

<when clause> <temporal operator> <when clause> WITHIN <window> [CONSTRAIN TO <query condition>]

The temporal operator in the above example, BEFORE, indicates that one event happens before another event. The complete set of temporal operators is:

WITHIN

The WITHIN clause specifies how closely in time the two events must occur. This time interval may be specified in seconds, minutes, hours or days:

WITHIN 12 seconds

This states that the second event must arrive within 12 seconds of the first event. Note that because of the vagaries of the scheduling algorithms used in the operating system and the underlying infrastructure, the 12 second interval will not be precisely enforced to the microsecond. The developer should think of the interval more appropriately as at least 12 seconds plus any delays associated with scheduling the rule for evaluation and then completing the evaluation.

If the interval specified in a WITHIN clause expires, the rule may need to take an action. This is accomplished by specifying a TIMEOUT section in the rule body. When the WITHIN interval expires, the statements included in the TIMEOUT section are executed. See TIMEOUT Statement for a detailed definition of TIMEOUT.

CONSTRAIN TO

The CONSTRAIN TO clause specifies joint conditions the correlated events must satisfy. In the example:

RULE multipleConditions
WHEN
  INSERT OCCURS ON Customer 
  BEFORE 
    INSERT OCCURS ON Order
    WITHIN 30 seconds 
    CONSTRAIN TO Order.customer == Customer.name

The customer name in the Customer event must match the customer in the Order event. If the condition is not satisfied by the pair of events, the rule body is not evaluated. For this example, these semantics correspond to the intuitive notion that it doesn’t make sense to process an event on one specific customer with an order for a different customer.

The CONSTRAIN TO clause may contain an arbitrarily complex logical expression comparing values in the two events being correlated. It is also possible to include expressions that constrain a single event although these are more properly specified in the WHERE clause associated with the individual event affording the system more optimization alternatives. For example:

RULE multipleConditions
WHEN 
  INSERT OCCURS ON Customer
  BEFORE
    INSERT OCCURS ON Order
    WITHIN 30 seconds
    CONSTRAIN TO Order.customer == Customer.name AND Customer.name == "paul"

is a valid CONSTRAIN TO clause but the preferred expression would be:

RULE multipleConditions
WHEN
  INSERT OCCURS ON Customer WHERE Customer.name == "paul"
  BEFORE 
    INSERT OCCURS ON Order
    WITHIN 30 seconds
    CONSTRAIN TO Order.customer == Customer.name
Compound Correlation

Temporal events can be composed to express more complex conditions. The conditions are evaluated left to right in a manner similar to arithmetic expression evaluation. For example:

RULE multipleConditions
WHEN
  INSERT OCCURS ON Customer
  BEFORE 
    INSERT OCCURS ON Order
    WITHIN 30 seconds 
    CONSTRAIN TO Order.customer == Customer.name
  BEFORE 
    INSERT OCCURS ON Shipment
    WITHIN 5 minutes 
    CONSTRAIN TO Order.orderNo == Shipment.orderNo

The system evaluates the first condition, then waits for the second condition and, after the first two conditions are satisfied, waits for the third condition. The precedence can be modified using parenthesis in a manner similar to arithmetic expressions:

RULE multipleConditions
WHEN 
  INSERT OCCURS ON Customer
  BEFORE
    (
       INSERT OCCURS ON Order
       BEFORE 
         INSERT OCCURS ON Shipment
         WITHIN 5 minutes 
         CONSTRAIN TO Order.orderNo == Shipment.orderNo
    )
WITHIN 30 seconds 
CONSTRAIN TO Order.customer == Customer.name

In this modified example, the precedence will cause the first condition to be evaluated and then combined with the result of evaluating the subsequent two conditions. Note that the CONSTRAIN TO and WITHIN clauses that previously followed the second condition have been moved to the end of the expression because the second and third conditions are being evaluated as a single condition with respect to the first temporal operator. Also note that the WITHIN clauses are now somewhat inconsistent since they specify a wait time of 5 minutes between the second and third conditions but only 30 seconds between the first condition and the COMBINATION of the second and third conditions.

WHEN statements with compound triggering conditions are a powerful tool for correlating data arriving from multiple IoT data streams. This is common in both consumer and industrial situations where data is being received from more than one sensor. For example, in a retail setting information may be received from BLE beacons, an indoor location system and the consumer’s smart-phone. In an industrial setting sensor data is being received from multiple sensors on separate channels and, possibly, separate machines where the readings need to be correlated in time.

Merging Events

Sometimes you have multiple event sources which are producing the same data. For example, you might have a source which provides sensor data and multiple instances of the source which provide data from different physical locations. In this case rather than writing separate rules to bind to each source, you can instead merge the events and process them in a single rule. This is done using the OR operator in a WHEN clause. For example:

RULE processMergedStream
WHEN
  EVENT OCCURS ON "/sources/EasternSource"
  OR
  EVENT OCCURS ON "/sources/WesternSource/"

In this example the rule body is evaluated whenever we receive a message from either EasternSource or WesternSource. The messages will be processed as they arrive, so they will be interleaved with each other without regard to where they originated.

The general form of a merging trigger condition is:

<when clause> OR <when clause>

It is possible to merge any event source with another, the only restriction is that both events must be bound to the same rule variable (which may require the use of aliases to achieve.

Detecting Missing Events

In addition to processing events as they occur, there may be times when a rule needs to run code when events don’t occur when expected. The WITHIN clause accomplishes this in the context of a correlation, but what about detecting the lack of events from a single event source? To do this you use the EXPECT clause. This clause has the following syntax:

<when clause> [EXPECT <expectation> <interval>]

Where expectation describes how to determine if an event is “missing” and interval specifies how long to wait for the expectation to be fulfilled. The possible expectations are:

Whenever the specified expectation is not fulfilled, the rule may need to take an action. This is accomplished by specifying a TIMEOUT section in the rule body. Whenever the expectation produces a timeout, the statements included in the TIMEOUT section are executed. See TIMEOUT Statement for a detailed definition of TIMEOUT.

For example, let’s say you have an MQTT source which should produce messages continuously. If you want to know whenever it stops producing messages for at least a minute you could use:

WHEN MESSAGE ARRIVES FROM myMQTTSource EXPECT EVERY 1 minute
... do work on message ...
TIMEOUT
... do work for lack of data ...

In this example, if the source stopped sending messages we would call the TIMEOUT section of the rule once every minute. If we just wanted it to be called once after the minute passes then we could use EXPECT WITHIN.

BEFORE Statement

Although WHEN is the most common statement used to trigger rule evaluation, the BEFORE statement may be used in situations where the rule should be applied BEFORE the triggering operation is applied to the automation database.

The BEFORE statement is only applicable to automation database updates and declares that the rule should be evaluated immediately before the new value is inserted, updated or deleted in the automation database. BEFORE rules are evaluated synchronously and the value returned by the rule is the value actually inserted or updated into the automation database. For DELETE operations the return value is ignored. A BEFORE statement can only reference a single event.

BEFORE clauses only operate on database events:

The BEFORE triggering condition is simplified in comparison to the WHEN clause:

BEFORE [INSERT | UPDATE | DELETE] ON <type> 

A special consideration with BEFORE clauses is that the rule has the opportunity to abandon the original database request. The request will be abandoned if the result returned by the rule is not the value to INSERT, UPDATE or DELETE but is a null value. This can be accomplished by setting the variable representing the target type to null at the end of rule execution or by using an explicit RETURN null statement.

TIMEOUT

The TIMEOUT statement is used to declare an alternate variant of the rule body which will be executed if the correlated event specified by the rule’s WHEN clause does not occur within the specified time period. For example:

RULE multipleConditions
WHEN
  INSERT OCCURS ON Customer WHERE Customer.name == "paul"
  BEFORE
    INSERT OCCURS ON Order
    WITHIN 30 seconds 
    CONSTRAIN TO Order.customer == Customer.name

    INSERT INTO Log(msg: "The event occurred.")

TIMEOUT

    INSERT INTO Log(msg: "The event did not occur.")

If the correlated event occurs in the 30 second time interval specified then the main body of the rule will be executed (in this case the first INSERT statement). However, if the event does not occur then the body specified after the TIMEOUT statement will be executed (in this case the second INSERT).

Variable References

Several VAIL statements support the use of variable references in order to dynamically specify the target of the statement. Variable references have the form @<variableName> and indicate that the system should use the current value of the variable as the name of the target resource. For example, the following code:

var variableName = "Sensor"
SELECT * FROM @variableName AS s

results in the system issuing a SELECT against the type “Sensor” and is equivalent to:

SELECT * FROM Sensor as s

The documentation of each statement will indicate when/if this construct is explicitly supported (no mention means that there is no support in that context).

SELECT

The select statement retrieves a set of objects and makes them available during rule evaluation. The value of the SELECT statement is the set of objects retrieved.

For example, the triggering event might contain a product id that must be mapped to a product name for use in rule processing. A SELECT statement can be used to obtain the product name from the product type.

var product = SELECT EXACTLY ONE name FROM Product WHERE productId == event.newValue.productId
var name = product.name

The overall syntax for the SELECT statement is:

SELECT [[SEQUENCE] | [ARRAY] | [EXACTLY] ONE] [* | <propertyBinding> [, <propertyBinding>]*] FROM [SOURCE | SERIES] <target>
[WITH <options>]
[WHERE <queryCondition>]
[GROUP BY <prop1> [, <propN>]*]
[ORDER BY <prop1> [ASC | DESC] [, <propN> [ASC | DESC]]*]
[[PROCESSED BY (<queryConstraint> | ALL) [CONTEXT TO <contextVar>] [WITHIN <timeInterval>]]
[[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR {
    <statements>
}]]

Query Results

The default query result is determined by the context of the SELECT statement. In most cases, the result will be an array of objects. However, when used as the target of a FOR, MAP, or FILTER statement, the SELECT will return an sequence of objects. The select will also produce a sequence if it is immediately followed by a processing block (and optional termination condition).

These defaults can be altered by explicitly declaring the result type of the SELECT statement as being one of:

propertyBinding

Property bindings are used to specify which properties of the target to retrieve or how to compute a property using properties of the target. For example, the query:

SELECT name, total = quantity * price FROM LineItem

Selects the “name” property from LineItem and computes the property “total” by multiplying the “quantity” and “price” properties. The expressions for computed properties support all of the standard operators and built-in functions. The full syntax for a property binding is:

<propertyName> [= <expression>]

target

The target identifies the type or source defined in the automation schema to which the SELECT will be applied. Series types are NOT currently supported. The target identifies the type and, optionally, contains a WITH clause containing any configuration information required for access to the type or source. The general syntax of a target is:

FROM [SOURCE | SERIES] <name> [AS <alias>] [WITH <options>]

The detailed syntax for types and sources is detailed below.

Select from a Type

The type is identified by its name and an optional alias name. For example, to retrieve a set of sensors:

SELECT FROM Sensor AS s

Two options may be specified in the WITH clause when selecting from a type:

The LIMIT and SKIP options are very convenient for rettreiving objects a page at a time. For example, to retrieve a set of objects 20 at a time initially set LIMIT=20 and SKIP 0 to retrieve the first 20 objects. To receive objects 21 through 40 set LIMIT=20 and SKIP=20; for obejcts 41 through 60 set LIMIT=20 and SKIP=40.

Select from a source

The source is identified by a phrase consisting of the keyword SOURCE and the name of the source. The optional WITH clause can be used to communicate configuration information required by the source. For example, to query the source named sampleRestSource and specify a request path as a configuration item:

SELECT FROM SOURCE sampleRestSource WITH PATH = "/myResource"

The result will be a list which contains the response data returned by the source (which is either a scalar, object, or set of objects). WHERE clauses are not supported when performing a SELECT on a source. Consult the documentation for the specific source to determine the configuration options supported by the WITH clause.

Select from a Series Type

The time series type is identified by its name and an optional alias name. For example, to retrieve the aggregate data from the series Velocity:

SELECT FROM SERIES Velocity AS v

The records returned will contain the group by property and any aggregate properties defined by the series type. There will be one record for each distinct value of the group by property. GROUP BY clauses are not supported since series types contain an implicit group by as part of their definition. WHERE clauses on Series select statements can operate on either the group by property or any of the aggregate properties, for example:

SELECT FROM SERIES Velocity WHERE speed > 100

or

SELECT FROM SERIES Velocity AS v WHERE v.id == "MyId"
Dynamic Target Names

There may be occasions when the name of the type, series or source is more conveniently specified as the content of a variable rather than a constant string value. For example, in general purpose procedures where the type that is the target of the query is supplied as a parameter value. Special syntax is available to indicate the target name is actually the string value referenced by a variable name:

var variableName = "Sensor"
SELECT * FROM @variableName AS s

In this example the name of the type is assigned to the variable: variableName. At runtime the value assigned to the variable is accessed and use as the target name for the select. At runtime the resulting query is:

SELECT * FROM Sensor AS s

Query options

The <options> are a set of one or more comma separated name, value pairs describing options that are supported by the type or source that is the target of the select. For example, a source might require the inclusion of a special URL identifying the service that supplies the data:

WITH url = https://services.vantiq.com/someService

Detailed documentation for WITH clause options are available in the documentation for each source for source-specific options and in the REST API Reference Guide for types.

queryCondition

Query conditions declare a logical constraint the selected objects must satisfy in order to be included in the result. The query condition is expressed in the WHERE clause.

Query conditions have a SQL-like structure so they are familiar to most database users. However, they do have a somewhat specialized syntax in order to optimize their processing in the context of rules. The query condition consists of one or more comparison clauses connected by the boolean operators AND, OR and NOT.

A comparison clause is defined as follows:

<propertyReference> <comparisonOperator> <constantValue>

Examples:

product.weight >= 5000

would select only products that weigh more than 5000 (kg)

product.type == "truck"

would select only products that are identified as trucks.

product.type == "truck" and product.weight >= 5000

would select products that are trucks weighing more then 5000 (kg)

The comparison operators supported are:

Operator Description Example
== exact match prop == 42
!= does not match prop != 42
> greater than prop > 42
>= greater than or equal to prop >= 42
< less than prop < 42
<= less than or equal to prop <= 42
in must be in the list of values prop in ["paul", "fred", "melanie", "susan"]
regex must match the regular expression pattern prop regex "^p\w*"

The GeoJSON comparison operators supported are:

geoNear GeoJSON value must be within the specified distance of a given GeoJSON object, e.g.,

prop geoNear { 
    geometry: { 
        type: "point", 
        coordinates: [ 90.834, 34.1987 ] 
    }, 
    minDistance: 100, 
    maxDistance: 1000 
}

geoIntersects GeoJSON value must intersect the given GeoJSON object, e.g.,

prop geoIntersects {
    geometry: { 
        type: "point",
        coordinates: [ 90.834, 34.1987 ]
    }
}

geoWithin GeoJSON value must be contained within the given GeoJSON object, e.g.,

prop geoWithin {
    geometry: {
        type: "polygon",
        coordinates: [[ [0, 0], [0, 10], [10, 10], [10, 0], [0, 0] ]]
    }
}

(Note that the GeoJSON comparison operators ignore any (optionally) provided altitude.)

The <constantValue> may be a constant or an expression that resolves to a constant during evaluation. See expressions below for more details.

Property names in a WHERE clause are NOT qualified with the name of the type if the statement refers to only a single type. The type is assumed to be the subject type of the SELECT, UPDATE or DELETE. In SELECT statements that have multiple types as subjects - cases in which a join is specified - properties must be explicitly qualified with the name of the type on which they are defined.

If the SELECT is operating on a source, only those query constraints supported by the specific source being queried are supported. This will generally be a subset of the queries that can be expressed using the general WHERE clause.

Dynamic queryCondition

Using the query conditions as described above, the comparison values can be variable values computed at the time the SELECT statement is executed. However, the comparison operators and the property names against which the values are compared are statically specified at the time the SELECT statement is authored.

An alternate syntax is used for dynamically specifying the property names and comparison operators. Specifically, the WHERE clause is specified in the form:

WHERE <variable>

The variable contains a query constraint represented as an object conforming to a JSON-encoded object as specified in API Reference Guide. For example, an object containing a query constraint that finds all employees with a firstName of ‘Steve’ and a salary greater than 100 would take the form:

{
    name: "steve", 
    salary: {
        "$gt": 100
    }
}

An example of all employees named ‘Steve’ OR with a salary greater than 100 would take the form:

{
    "$or": [
        { name:   "Steve" }, 
        { salary: { "$gt": 100 }}
    ]
}

GROUP BY

The GROUP BY clause is used to aggregate the results of a select for sets of objects having the same values for the specified properties. For example:

SELECT customerId, count=COUNT() FROM Order GROUP BY customerId

Selects the count of orders for each unique value of “customerId”. Without the GROUP BY clause the system will not know how to apply the aggregate function and the results will be undefined.

ORDER BY

The ORDER BY clause is used to sort the results of the SELECT.

SELECT * FROM <type> WHERE <constraint> ORDER BY <prop1> [ASC | DESC], ...

The results are sorted on the properties specified in the ORDER BY clause. The order of the sort of each property is specified as ascending with the ASC modifier or descending with the DESC modifier. The default ordering is ascending.

Properties declared with one of the types:

may be referenced in the ORDER BY clause. Decimal, Currency, ResourceReference, Object and Arrays of any type may NOT be included in the ORDER BY clause.

Processed By

SELECT supports remote invocation via the standard PROCESSED BY clause. As with local selects, a remote select will produce a single, merged result which can be processed as an array or sequence as described below. Note that all query options are evaluated locally on each node which executes the statement. This means that options such as GROUP BY or ORDER BY are not evaluated globally for the entire result set.

Variable Declarations

Variables created within the body of a rule or procedure must be declared before they are used. A variable is declared using the Javascript variable declaration syntax:

var <variableName>

This creates a new variable named variableName.

As a convenience, a variable may be declared and assigned a value in a single statement using the notation:

var <variableName> = <expression>

Variables are globally visible within the rule or procedure in which they are defined.

Variable names must follow standard VAIL naming conventions. A name may consist of alphanumeric characters and the underscore. The name may not begin with a number.

Variable names are case sensitive.

Implicit Variable Declarations

As documented in the section on the WHEN statement, variables are automatically defined that reference the events or event aliases that trigger rule execution. Similarly, the SELECT statement automatically declares a variable with the same name as the target type (or its alias) identified in the FROM clause. Note that this only applies if the SELECT references a single type. If the SELECT describes a JOIN, the result must be explicitly assigned to a variable.

A procedure implicitly defines variables for each of its parameters.

The FOR statement implicitly defines a variable for its iteration variable if the iteration variable has not already been defined.

Assignment

A traditional assignment statement is available to assign a value to a variable. The example assignment:

myVar = 5 + 10

will assign the existing variable named myVar to the integer value 15. More generally:

<variableName> = <expression>

VAIL Types

VAIL supports the standard types used throughout Vantiq as well as additional extended types optimized for use in series computations.

Standard Vantiq Types

The standard types are more completely defined in Scalar Base Types.

The types do not necessarily correspond directly to SQL types or to Javascript types. For example, there is no Javascript-like Number type that supports both integer and floating point numbers. However, the general behavior of strings, integers, reals, dates and objects will be familiar to both Javascript and SQL users.

Extended Types

Arrays and Sequences

VAIL supports collections in the form of Arrays, similar to Javascript arrays, and Sequences.

An array is a dynamically expanding set of elements with each element placed in a “slot” in the array. The slots are directly address with an index number. The slots are numbered from 0 to N where N is the size of the array - 1. New elements may be added to either the beginning or end of the array. Arrays may the iterated over using the FOR statement. We sometimes use the term list to refer to an array.

Arrays are stored in memory. Therefore, the maximum size of an array is limited. VANTIQ STRONGLY recommends that arrays contain less than 10,000 elements. In a future release the 10,000 element limit on the size of an array WILL BE ENFORCED by the runtime system.

A sequence is also a collection of elements but, in contrast to arrays, sequences are:

Expressions

An expression computes a value by applying operators to values and procedure invocations.

Values

VAIL supports the following values:

References to the properties of an object take the form:

<variableName>.<propertyName>

or

<variableName>[<propertyName>]

The entries in an array or list can be referenced using the index operator: []

<variableName>[<indexValue>]

References to properties and elements of an array can be combined:

<variableName[<indexValue>].<propertyName>[<indexValue].<subPropName>

Unqualified property names may only be used in queryConstraints where the type reference is unambiguous. They may not be used in IF or THEN clauses.

String constants are delimited by double quotes.

Procedure invocations take the conventional forms:

<procedureName>(<arg1>, ... <argN>)

or the parameters can be specified by name rather than by position:

<procedureName>(<p1>: <v1>, ... <pN>:<vN>)

The “procedureName” is the fully qualified name of the procedure, including its service, if any. For example, to invoke the procedure “changeSalary” in the service “EmployeeUtils” you would use:

EmployeeUtils.changeSalary()

In addition, many of the built-in procedures can be called “method style” similar to Javascript method invocations. In these cases, the object on which the “method” is called is always the first parameter to the procedure.

<object>.<procedureName>(<arg2>, ... <argN>)

A classic example is the length procedure applied to a string which is invoked as a procedure with the string as its first parameter:

var myString = "a string"
var l = length(myString)

Using the method style, the same procedure call takes the form:

var myString = "a string"
var l = myString.length()

Comparison Operators

Operator Description Example
== Equality expr1 == expr2
!= Inequality expr1 != expr2
> Greater Than expr1 > expr2
>= Greater Than or Equal expr1 >= expr2
< Less Than expr1 < expr2
<= Less Than or Equal expr1 <= expr2

Logical Operators

Operator Description Example
&& Logical And expr1 && expr2
|| Logical Or expr1 || expr2
! Logical Not !expr

Data Operators

A set of operators that can be applied to numeric values and string values are available for use in expressions:

Operator Description Example
+ Addition expr1 + expr2
- Subtraction expr1 - expr2
* Multiplication expr1 * expr2
/ Division expr1 / expr2
** Exponentiation expr1 ** expr2
% Modulo expr1 % expr2
++ Postfix Increment expr++
Postfix Decrement expr--

Assignment Operators

Operator Description Example Equivalent
= Assignment varName = expr N/A
+= Addition Assignment varName += expr varName = varName + expr
-= Subtraction Assignment varName -= expr varName = varName - expr
*= Multiplication Assignment varName *= expr varName = varName * expr
/= Division Assignment varName /= expr varName = varName / expr

Decimal and Currency Support

The arithmetic operators are supported on the Currency and Decimal types with the exception of the remainder (%) operator. Arithmetic on mixed numeric types is supported between Decimal/Currency values and any Integer, Real, Decimal or Currency values whether stored in a variable or expressed as a constant. For example, assuming myDec is a variable containing a Decimal value and myCur is a variable containing a currency value, the following expressions are supported:

myDec * myDec
myDec + 12
myDec - 100
myDec * 2

myCur * myCur
myCur + 12
myCur - 100
2 * myCur
myCur / 4
myCur ** 2

"USD:12.0" + myCur
"20.0" * myDec

Comparison (relational) operators always determine the type of the comparison by the type of the left operand. Thus, if the left operand is a Decimal or Currency value, the right operand is converted to a Decimal or Currency value and the comparison made. However, if the left operand is Integer, Real or String an attempt is made to convert the right operand to the corresponding type. If the right operand is a Decimal or Currency value, this conversion will fail resulting in a ClassCastException. The simplest approach is to always make a Decimal or Currency value the left operand in any comparison. If this is not possible, the left operand should be explicitly converted to Decimal using the toDecimal() function or to Currency using the toCurrency() function. Examples of valid comparisons to Decimal and Currency:

myDec >= "100.42"
myCur < "USD:100.00"
toDecimal("100.42") < myDec
toCurrency("USD:100.00") == "USD:100.42"
myCur == myCur

The following are examples of unsupported comparisons:

"USD:100.00" <= myCur
50.0 < "22.25"

When two decimal or currency values are involved in the computation, the scale of the result is set as follows:

Of course, if the result value is then assigned to a property whose type is Decimal or Currency, the scale is set to the scale declared for the property.

Built-in Procedures

String Procedures

A set of predefined string procedures are available to conveniently operate on strings.

Similar to Javascript String methods:

Extended string procedures that have no correspondence in standard Javascript:

String Formatting

The format procedure takes a set of values, formats them and then inserts the formatted strings into the specified pattern producing the formatted string.

In its simplest form the pattern contains a set of placeholders and the set of values are scalar and object values. The pattern string contains placeholders of the form {n} where n is the index of the parameter to be substituted into that location in the pattern. The index starts at 0 identifying the leftmost parameter and incrementing by one to identify each subsequent parameter. The values are formatted using a default style. For example,

format("Customer {0} has {2} orders outstanding and {1} invoices outstanding.", "Acme", 6, 8)

will produce the string value:

Customer Acme has 8 orders outstanding and 6 invoices outstanding.

Most applications of format() need be no more complicated than this example. However, if more precise formatting is desired optional elements can be added to the placeholders to more finely control formatting. The format of placeholders with optional elements:

{ <parameterIndex>, <formatType> }
{ <parameterIndex>, <formatType>, <formatStyle>}

formatType:
    number
    date
    time
    choice

formatStyle:
    short
    medium
    long
    full
    integer
    currency
    percent
    <subformat>

If only the formatType is added to the placeholder, the corresponding parameter is formatted using the formatting for the type specified. If both formatType and formatStyle are specified, the parameter is formatted as the type specified and in the style specified.

A special case that requires additional discussion is the use of a subformat. The subformat supports detailed control over the formatting of number, date, time and choice. If a subformat is used, be aware that leading and trailing spaces between the separating commas and the pattern are considered part of the pattern.

Number Formatting

A number format contains either a single format for positive and negative numbers, OR a sequence of two patterns separated by a ; (semi-colon), with the first pattern used for positive numbers and the second pattern used for negative numbers.

The symbols available for use in a pattern:

Symbol Description
0 a single digit, leading and trailing zeros are displayed.
# a single digit, leading and trailing zeros are suppressed.
. decimal separator
- minus sign
, grouping separator
E separates mantissa and exponent in scientific notation
% multiply by 100 then display as a percentage
escape pattern symbols

Examples:

format("{0, number, ###,###;(###,###)}", 123456)

    123,456

format("{0, number, ###,###;(###,###)}", -123456)

    (123,456)

format("{0, number, 000000}", 1000)

    001000

format("{0, number, ######}", 1000)

    1000
Date and Time Formatting

A date format contains a single string containing the following symbols to describe the date format:

Symbol Description
G era designator
y year
Y week year
M month in year
w week in year
W week in month
D day in year
d day in month
F day in week
E day name (Monday through Sunday)
u number of day in week (Monday == 1)
a am/pm
H hour in day (0-23)
k hour in day (1-24)
K hour in am/pm (0-11)
h hour in am/pm (1-12)
m minute in hour
z time zone
Z time zone RFC 822
X time zone ISO 8601

Examples: Assume myDate contains a representation of the date: 2016-08-06 12:19:34 pm PDT

format("{0, date, yyyy-MMM-dd HH:mm z}", myDate)

    2016-Aug-06 12:19 PDT

format("{0, date, EEE, dd MMMMMM yyyy HH:mm:ss}", myDate)

    Sat, 06 August 2016 12:19:34    
Choice Formatting

Choice formatting selects one of the formats based on the numeric value of the parameter. If the pattern takes the form: <value>#, the choice is selected if the parameter value matches the pattern value. If the pattern takes the form: <value><, the choice is selected if the value is greater than the value specified in the pattern but less than or equal to any subsequent value specified in the pattern. Each choice in the pattern is separated by the | character. Note that trailing white space is preserved. For example, the space after RED here 0#RED | 1#BLUE will be preserved and so is different than specifying 0#RED| 1#BLUE.

The behavior will become clear in the examples:

format("{0, choice, 0#RED| 1#BLUE| 2#GREEN| 3#ORANGE}", 2)

    GREEN

format("{0, choice, 0#RED| 1#BLUE| 2#GREEN| 3#ORANGE}", 0)

    RED

format("{0, choice, 0#no items| 1#one item| 1<{0}items}", 1)

    one item

format("{0, choice, 0#no items| 1#one item| 1<{0}items", 4)

    4 items

Integer and Real Procedures

for (num IN range(1, 10, 1)) {
    <statements>
}

This will iterate over the integers from 1 to 9.

The range function makes it very easy to iterate over the members of a list using the syntax:

var myList = [1, 2, 3, 4]
var total = 0
for (num in range(0, myList.size(), 1)) {
    total = total + myList[num]
}

The resulting value of total is 10.

Array/List Procedures

Similar to Javascript Array methods:

Arrays support additional convenience procedures that are not Javascript-like:

Object Procedures

Similar to Javascript Object methods:

Dates and Intervals

Time is an important element of most automation systems and particularly characteristic of a time series. Dates and times must be massaged in a variety of ways to get the date into the form ultimately required for processing. VAIL supports two time related abstractions: date and interval.

Conversion from a higher precision representation to a lower precision representation truncates higher precision components of the date. Built-in procedures are available to convert dates from one representation to another representation.

Intervals can be specified in one of the following units:

Examples:

23 milliseconds
100 seconds
5 weeks

An interval is represented as the number of milliseconds in the interval.

Date and Interval Procedures

The following procedures apply to dates and/or intervals:

date(now(), "date", "ISO")

Another example: convert a date from epochMilliseconds to epochDays effectively truncating the time from the date to leave just the day component:

var ems = date(now(), "date", "epochMilliseconds")
date(ems, "epochMilliseconds", "epochDays")

Another typical example is converting a date from the standard representation to epochMinutes as a first step in constructing a series containing one minute windows:

date(now(), "date", "epochMinutes")

The sourceRepresentation and destinationRepresentation parameters may be one of the following string values:

The following procedures extract portions of a date (as opposed to truncating the low order components of the date as explained for the date() operation). These procedures are similar to the corresponding Javascript date methods:

In addition to the above, a number of date procedures are available that do not correspond to Javascript Date methods:

 var millisecondsInOneDay = durationInMillis("P1D")

Intervals may be added and subtracted from dates using the following methods defined on the standard date type:

Example:

var twoDaysFromNow = now().plusMillis(2 days)

The use of the method style invocation is required because plusMillis is a method defined on the underlying date type and not a registered VAIL procedure.

ResourceReference Procedures

Regular Expressions

Regular expression patterns are supported as a VAIL data type for use in procedures that support string search using a regular expression.

A regular expression pattern may be created by calling the RegExp procedure.

The regular expression can be used as the value of the pattern parameter for the match, replace and search procedures.

Utility Procedures

For convenience, a number of commonly useful procedures are predefined.

Procedures that are similar to Javascript global functions:

Procedures that do NOT correspond to similarly named Javascript functions:

Math

A collection of basic math functions are available for use in rules and procedures. A summary of the math functions we currently support:

Available Trig functions (NOTE: all input values must be in radians):

A set of standard constants have been implemented as parameterless procedures:

Aggregates

A number of aggregate functions are avialable for use in time series types. A summary of the aggregate functions:

Note that the sum, avg, rise, min, and max functions support only Integer, Real, and DateTime data types.

More details on time series operations can be found in the Time Series Guide.

Built-In Services

As described in the Services Section of the Resource Guide, services are a resource used to organize collections of procedures. The following is a list of Built-In Services:

ActivityPattern

The ActivityPattern service contains procedures that generate the resources for each type of activity in a collaboration type. For instance, when a Notification activity appears in a collaboration type, the code that generates the procedure that fires the notification off and the rule that processes the responses is ActivityPattern.notificationActivityPattern. These are used only for collaboration resource generation and are not intended to be used outside of collaboration type creation and modification.

Chat

The Chat service implements a collection of procedures for interacting with the messaging component of the Vantiq Mobile Apps. The Chat service contains the following procedures:

CollaborationGeneration

The Collaboration Generation Service contains a collection of procedures used to generate resources for collaboration types. These are not documented and are for internal use only.

CollaborationUtils

The CollaborationUtils Service contains a collection of procedures used to manipulate active collaborations, including the following:

Deployment

The Deployment service contains a collection of procedures used to implement the configuration deployment tool in the Developer Portal.

LocationTracking

The LocationTracking service contains a collection of procedures used to implement location tracking on the Vantiq Mobile Apps. The LocationTracking Service contains the following procedures:

Notification

The Notification Service is used to send notification to the Vantiq Mobile Apps. The Notification Service includes the following procedures:

Recommend

The Recommend Service contains the built in recommenders. They all follow a similar pattern, in which they take 4 parameters:

Currently there are 2 built in recommenders:

Utils

The Utils Service contains a collection of useful procedures, including:

IF-THEN-ELSE-END

The IF statement provides conditional execution.

The IF clause specifies a condition that must be satisfied by the current rule evaluation context. If the condition is satisfied the THEN clause is evaluated. If the condition is not satisfied, optional ELSE IF conditions are evaluated. If none are satisfied, the optional ELSE clause is evaluated.

if (<logicalExpression>) {
    <then statements>
} else if (<logicalExpression>) {
    <else statements> 
} else {
    <else statements>
}

The <logicalExpression> must result in a boolean value. It may contain expressions and comparison operators combined using the Boolean operators AND, OR and NOT.

If the IF or ELSE IF clause evaluates to true the corresponding THEN clause is executed. The THEN clause may contain any number of statements. The IF/ELSE IF clauses are evaluated in order. If all IF clauses evaluate to false, then the ELSE clause is evaluated if it exists. The ELSE clause may also contain any number of statements.

Note that statements embedded in the THEN and ELSE such as insert, update, delete and publish may cause additional events to be triggered causing related rules to be evaluated. Similarly, component invocations may cause additional events if they change the state of the automation database or publish user defined events.

FOR

The FOR statement iterates over the elements of a collection. The item is the iteration variable that will be populated, in turn, by each element in the collection. The collection may be an array, a set, a SELECT statement, a range expression or the properties of an object.

for (<item> in <collection> [until <terminatingCondition>]) {
    <statements>
}

The FOR statement executes once for each object in <set> assigning the value of that object to <item>. The <statements> in the FOR body are then executed for that item. The statements may reference the value by referencing <item>:

for (o in Orders) {
    if (o.total > 1000) {
        PUBLISH { message: "Thank you for your order" }
            TO SOURCE companyEmail
            USING { recipients: [ o.customerEmail ] }
    }
}

This will inspect every order and send thank you emails to each customer with a total value greater than 1,000.

A SELECT example:

for (o in SELECT * FROM Orders WHERE total > 1000) {
    PUBLISH { message: "Thank you for your order" }
        TO SOURCE companyEmail
        USING { recipients: [ o.customerEmail ] }
}

This will select orders with a total greater than 1,000 and send each customer a thank you email.

A range expression may be used to iterate over a range of integer values. Example:

var total = 0
for (i in range(1,10,1)) {
    total += i
}

This iterates over the integers from 1 up to (but not including) 10, assigning each integer to i. The resulting total will be the integer value: 45.

Finally, the FOR statement supports iteration over the properties of an object. Example:

var foundPaul = false
var person = {name: "paul", address: "11 Main Street", salary: 200}
for (prop in person) {
    if (prop.key == "name" && prop.value == "paul") {
        foundPaul = true
    }
}
if (foundPaul == true) {
    person.status = "employee"
}

Although a bit contrived the above code fragment will search the object looking for the property named name and then check if the value of the property is the string paul. Note that if the test is successful, a new property is added to the obejct. The next time a FOR statement is used to iterate over the properties of the object the added employee property will be processed in the body of the FOR. Iterating over the properties of an obejct can be very useful in cases where you are processing objects whose structure may change from instance to instance.

The terminatingCondition allows the user to specify a condition which, when satisfied, will terminate the execution of the FOR loop even if there are more items in the set over which the FOR is iterating. For example, to terminate FOR execution as soon as the value 5 is produced:

var total = 0
for (i in range(1,10,1) until i == 5) {
    total += i
}

The value of total on completion is 10 computed as: 1 + 2 + 3 + 4. The FOR terminated as soon as x was assigned the value 5 and before the body of the FOR was executed with the value 5.

FILTER

The FILTER statement accepts a set as its input and iterates over the elements in the set producing a new set as its result. The condition specifies a condition that terminates iteration over the set as soon as it evaluates to true resulting in all elements of the set that have yet to be processed being ignored. An item is placed in the result set if the boolean value resulting from executing statements is true. An item is NOT placed in the result set if the boolean value from executing statements is false.

FILTER (<item> IN <set> [UNTIL <condition>])
{ 
    <statements>
}

The FILTER statement may be applied to items of any type (though most typically it is applied to objects from the automation model).

For example:

var user = SELECT * From MyUser as user WHERE name = "paul"
var locFilter = FILTER (r in SELECT * FROM PossibleActions) {
    if (r.location == user.location) {
        return true
    }
    else {
        return false
    }
}

The result of the FILTER operation might then be further refined by applying another filter (note the more compact form):

var typeFilter = FILTER (r in locFilter) {
    return r.type == user.type
}

The first filter will iterate over all objects in “PossibleActions” returning a result consisting of all possible actions that are appropriate in the user’s location.

The second filter will iterate over the results in locFilter applying another constraint to further reduce the set of possible actions to only those that are relevant to the user type.

MAP

The MAP statement accepts a set as its input and iterates over the elements in the set producing a new set that contains all the elements returned by the MAP body. The body of the MAP statement may modify the value of the elements in set or even produce entirely new elements and it is these modified elements that are placed in the result set.

The following example gives all Employees a 10% raise with the result of the MAP statement the set of Employees and their new salaries.

var employeesWithRaises = MAP (r in SELECT * FROM Employee) {
    r.salary += (r.salary * 0.10)
}

This has produced a new set containing all the employees with their raises. Additional processing is required to record the raises in the database. For example, the result set could be processed by a subsequent FOR statement to update the database:

    FOR (emp in employeesWithRaises) {
        UPDATE Employees(salary: emp.salary) WHERE name == emp.name
    }

INSERT

The INSERT statement inserts a new object into a type using the syntax:

INSERT [INTO] <typeName>(<valueSet>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The valueSet may be a list of name, value pairs in the form:

INSERT [INTO] <typeName>(<propertyName>: <value> [, <propertyName> : <value>]...)

or an object constant:

INSERT [INTO] <typeName>({<propertyName>: <value> [, <propertyName> : <value>]...})

or a reference to a variable containing the object to insert:

var <myVar> = {<propertyName>: <value> [, <propertyName> : <value>]...}
INSERT [INTO] <typeName](<myVar>)

For example, to insert a new customer into the Customer type:

INSERT Customer(name: "aCustomerName", address: "1400 Main St, SomeCity AK")

will insert a customer consisting of a name and address into the Customer type.

An alternate syntax is available for those more comfortable with SQL:

INSERT INTO Customer(name = "aCustomerName", address = "1400 Main St, SomeCity AK")

Note that the language does NOT support the standard SQL syntax of:

INSERT INTO <type> (<col1>, ...) VALUES(<val1>, ...)

The same example using a reference to a variable containing the object to insert:

var obj = { name: "aCustomerName", address: "1400 Main St, SomeCity AK" }
INSERT Customer(obj)

Series

The INSERT statement inserts a new object into a series using the syntax:

INSERT [INTO] SERIES <seriesName>(<propertyName>: <value> [, <propertyName> : <value>]...)

or

INSERT [INTO] SERIES <seriesName>(<propertyName> = <value> [, <propertyName> = <value>]...)

The window property and key properties defined in the series declaration MUST be assigned values in the insert statement.

The INSERT statement supports the incremental computation of new series values based on the values being inserted. This is very convenient for updating a series with data arriving from a streaming source. Typically, the series contains aggregate values that are incrementally computed and stored in the series. The currently supported aggregate operations are:

Note that the sum, avg, rise, min, and max functions support only Integer, Real, and DateTime data types.

Given a series defined as follows:

CREATE SERIES Velocity(window Integer, id String, speed Real = AVG(speed))
    WINDOW OF 5 MINUTES INCREMENTING EVERY 1 MINUTE
    SERIES LENGTH 60 MINUTES
    WITH naturalKey = ["id"], window = "window"

Inserting a new object into the series would take the form:

INSERT INTO Velocity(window: message.ts, id: message.id, speed:  mesage.velocity)

The example will update the average speed being accumulated in the window identified by message.ts for the object identified by message.id. If an entry does not existing for the window and key combination, a new entry is inserted.

If the series has reached its maximum length and the newly inserted object would cause the size of the series to increase, the oldest object in the series is removed and the new object is inserted.

If an existing value that is not a computed value exists in the series for the window and key values specified, the newly inserted object will REPLACE the existing value in the series instance.

Variable References

INSERT supports the use of a variable reference (aka @<variableName>) when specifying the name of the type or series which is the target of the INSERT.

Processed By

INSERT supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the INSERT was executed.

UPDATE

The UPDATE statement updates the specified object(s) of the specified type using the syntax:

UPDATE <typeName>(<valueSet>) WHERE <queryCondition>
[[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]] |
ON ERROR (<errorVar>) {
    <statements>
}]

The valueSet may be a list of name, value pairs in the form:

UPDATE <typeName>(<propertyName>: <value> [, <propertyName> : <value>]...)
    WHERE <queryCondition>

or an object constant:

UDPATE <typeName]({<propertyName>: <value> [, <propertyName> : <value>]...})
    WHERE <queryCondition>

or a reference to a variable containing the object to update:

var <myVar> = {<propertyName>: <value> [, <propertyName> : <value>]...}
UPDATE <typeName](<myVar>)
    WHERE <queryCondition>

For example, to update the name of an existing customer found in the Customer type:

UPDATE Customer(name: "newCustomerName") WHERE name == "aCustomerName"

The existing Customer with the name “aCustomerName” is assigned the new name “newCustomerName”.

The same example using a reference to a variable containing the object to update:

var obj = { name: "newCustomerName" }
UPDATE Customer(obj) WHERE name == "aCustomerName"

A local update with an error handling block:

UPDATE Customer(status: "closed") WHERE status == "closing" ON ERROR (errValue) {
    ... perform error recording/recovery ...
}

Series

The UPDATE statement does not support series types. Use INSERT to add data to an existing series.

Variable References

UPDATE supports the use of a variable reference (aka @<variableName>) when specifying the name of the type which is the target of the update.

Processed By

UPDATE supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the UPDATED was executed.

The UPDATE statement identifies the objects to be updated by a logical constraint. The update occurs by selecting all objects that satisfy the condition and then applying the update to each object. This algorithm is extended to the distributed case in a manner that provides for similar semantics. A distributed select is invoked to obtain all objects that satisfy the update constraint. For each qualifying object an update is executed. Because of these semantics, care must be taken to carefully select target objects from only those nodes for which updates are intended.

UPSERT

The UPSERT statement performs one of two functions on the specified object of the specified type:

The type specified MUST have a naturalKey declared. The value of the naturalKey MUST be included in the specified object. If the type does not declare a naturalKey or the value for the naturalKey is not included in the upsert values, an error will be reported.

The syntax of the UPSERT statement:

UPSERT <typeName>({<propertyName>: <value> [, <propertyName> : <value>]...})
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Example:

UPSERT Customer(name: "customerName", address: "11 Main Street")

Assuming Customer has declared name as its natural key and a Customer with name == customerName does not exist, this new Customer will be inserted. However, if a Customer with the name, customerName does exist, the existing Customer’s address will be updated to “11 Main Street”.

Variable References

UPSERT supports the use of a variable reference (aka @<variableName>) when specifying the name of the type which is the target of the UPSERT.

Processed By

UPSERT supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the UPSERT was executed.

DELETE

Type

The DELETE statement deletes the specified object(s) of the specified type with the following syntax:

DELETE <typeName> WHERE <queryConstraint>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

For example, delete the customer updated in the UPDATE statement example:

DELETE Customer WHERE name == "newCustomerName"

Series

The DELETE statement deletes ALL objects in a series with the following syntax:

DELETE SERIES <seriesName>

A WHERE clause may not be specified.

Variable References

DELETE supports the use of a variable reference (aka @<variableName>) when specifying the name of the type or series which is the target of the DELETE.

Processed By

DELETE supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the DELETE was executed.

PUBLISH

The PUBLISH statement allows the user to publish events on a specified topic or deliver a message to a specified source. The general syntax of the PUBLISH statement:

PUBLISH <message> TO [TOPIC <topic> [SCHEDULE <schedule>] | SOURCE <source> USING <config>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

PUBLISH Topic

The syntax for publishing a user defined event:

PUBLISH <message> TO TOPIC <topic> [SCHEDULE <schedule>]

The <topic> must be a valid user defined topic. User defined topics must start with a slash(‘/’) and may not start with: ‘/type’, ‘/property’ or ‘/system’.

The <message> is generally an object that contains the data to be associated with the published event as its newValue value.

The event may be processed by another rule by specifying the appropriate WHEN clause:

RULE sampleReceiver
WHEN PUBLISH OCCURS ON <topic>
...

The published event is referenced via the built-in variable event. The <message> is referenced by: event.newValue. A simple example:

First publish an event from a rule (or procedure):

RULE myPublishingRule
WHEN INSERT OCCURS ON myType
PUBLISH { name: "granite" } TO TOPIC "/myTopic/subtopic/new" 

Then trigger on this event in another rule:

RULE sampleReceiver
WHEN PUBLISH OCCURS ON "/myTopic/subtopic/new"
if (event.newValue.name == "granite") {
    INSERT EventLog(event: "user defined topic was published")
}
Scheduled Events

The PUBLISH statement can be used to create a Scheduled Event which will be delivered at a future point in time (and possibly re-delivered periodically). This is done by adding the optional SCHEDULE clause to the PUBLISH statement. For example:

PUBLISH { name: "granite" } TO TOPIC "/myTopic/subtopic/new" SCHEDULE {interval: 1 minute}

This statement will publish the event from the previous example, but instead of doing so immediately, the event will be delivered 1 minute after the statement executes. The <schedule> argument must be an expression which produces an Object. This can be an Object constant (as shown above) or it could be any other expression such as a variable reference or the invocation of a procedure. The properties of the schedule object vary depending on whether the event should be scheduled for one-time or periodic delivery.

The previous example showed the creation of a one-time event, here we are creating a periodic event:

PUBLISH { name: "granite" } TO TOPIC "/myTopic/subtopic/new" 
SCHEDULE {name: "graniteEvent", periodic: true, interval: 5 minutes}

This event will be initially delivered 5 minutes after the request and will be repeated once every 5 minutes until it is deleted.

Once created via a PUBLISH statement, scheduled events are stored as system resource instances and can be manipulated as such. So to delete the previously created event you would use:

DELETE scheduledevents WHERE name == "graniteEvent"

PUBLISH Message

The syntax for publishing a message to a source:

PUBLISH <message> TO SOURCE <source> USING <config>

The message format is source dependent and represents the data to be sent to the destination by the source. The destination could be any type of source: email, sms, messaging, REST service, etc. The source is the name of the source that will deliver the message. The config is an object containing the configuration data required by the source to publish the message. See the documentation for the specific source type for details of supported message formats and config formats.

Example:

PUBLISH { id:Device.id, condition:"The device is no longer operating within allowed tolerances." }
  TO SOURCE remoteSource
  USING { topic: "/myTopics/device/outOfRange" }

Variable References

PUBLISH supports the use of a variable reference (aka @<variableName>) when specifying the name of the topic or source which is the target of the PUBLISH.

For example, in order to provide a topic dynamically use the VAIL dereference operator. For example:

VAR myTopic = "/myTopic/subtopic/new"
PUBLISH { name: "granite" } TO TOPIC @myTopic

In order to provide a source name dynamically use the VAIL dereference operator. For example:

VAR mySource = "remoteSource"
PUBLISH { id:Device.id, condition:"The device is no longer operating within allowed tolerances." }
  TO SOURCE @mySource
  USING { topic: "/myTopics/device/outOfRange" }

Processed By

PUBLISH supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the PUBLISH was executed.

EXECUTE

The EXECUTE statement is used to explicitly invoke a procedure. However, since procedures can be invoked within expressions, EXECUTE is optimized for executing procedures in remote nodes using the PROCESSED BY clauses.

The EXECUTE syntax:

EXECUTE <procedureName>(<parameters>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Parameters may be specified either positionally or by name. However, all parameters must be either named or positional. A mix of named and positional parameters is not supported.

For example, to invoke the chooseOption procedure with two parameters, name and optionType, on remote nodes of type dataAcquisition the following execute statement could be used:

EXECUTE chooseOption("myName", 11)
PROCESSED BY properties.type == "dataAcquisition"

Similarly, to supply named parameters for this same invocation:

EXECUTE chooseOption(name: "myName", optionType: 11)
PROCESSED BY properties.type == "dataAcquisition"

Dynamic Procedure Selection

There are times when the name of the procedure to be executed is not known until runtime. In such cases, the procedure name must be selected dynamically. EXECUTE supports a special notation for selecting a procedure name dynamically:

EXECUTE * (<parameters>) WHERE <procedureConstraint>

In this dynamic form of EXECUTE, the procedure name is not specified after the EXECUTE keyword. Only the actual parameters are specified. The procedure is selected by querying the procedures resource for a set of instances that match the specified procedure constraint. As a simple example select a procedure based on the value in a variable:

var procName = "paul"
if (a == 1) {
    procName = "sharon"
}
EXECUTE * ("parm1", "parm2") WHERE name == procName

This will execute either a procedure named: paul or a procedure named: sharon depending on the value of the variable: a.

If the WHERE clause evaluates to a set of procedure names, all the procedures in the set will be executed.

The result of issuing an EXECUTE statement with a WHERE clause will ALWAYS be an array of procedure results, one entry for each procedure that is executed. The order of execution is indeterminate since each procedure may be executed asynchronously.

A convenient technique for selecting procedures on criteria other than name is to qualify the request on the value of properties in the procedures resource. The properties value can be set when the procedure is defined using the WITH clause on the PROCEDURE statement. For example, if properties for a procedure contains an object in the following form:

{ role: "manager" }

The following EXECUTE statement will invoke all procedures that have the value “manager” assigned as their role:

EXECUTE * (1, 2, 3) WHERE properties.rule == "manager"

See PROCEDURE for a detailed description of assigning values to properties.

Processed By

EXECUTE supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the EXECUTE was executed, even if the procedure returns a sequence. Returning “embedded” sequences (a sequence contained inside an object or array) is not supported for remote invocations.

TRY-CATCH-FINALLY

The TRY statement provides a way to CATCH (intercept) errors that occur while running a rule or procedure. When an error occurs during the execution of a rule or procedure the system will generate an exception which causes the execution to terminate with an entry written to the current namespace’s error log. However, if the error occurs in the context of a TRY statement then the error will be delivered to the CATCH block and processing will continue from there as if the error had not occurred. The general form of the statement is:

try {
    <statements>
} catch(errorVar) {
    <error processing statements>
} [finally {
    <final processing statements>
}]

Upon completion of the statements in the CATCH block processing will continue with any statements that follow the TRY statement, unless the error processing statements themselves generate an error (at which point control would pass to the CATCH block of any enclosing TRY statement or would result in termination if no such statement exists).

In addition to the CATCH block (which is mandatory, though it may be empty) there may be an optional FINALLY block. The statements found in this block will be executed any time the block is exited, whether that is due to normal completion of the statement or an error that has occurred during that execution.

The error value is delivered to the CATCH block via the declared error variable (errorVar) and contains an object with the following properties:

RETURN

Rules and procedures return a value when their execution completes. By default, the return value is the last value computed by the rule or procedure. In order to make return values more explicit the RETURN statement may be used to specify the returned value. However, the RETURN statement MUST be the last statement in the body of the rule. The RETURN statement CANNOT be used to terminate execution of the rule earlier in the rule body. The form of the RETURN statement:

return "This string will be the return value for the enclosing rule or procedure"

CREATE

The CREATE statement supports creating new types, rules, sources and other automation artifacts:

The new artifact can be specified using one of two alternate representations:

Both variants are described below.

Variable References

CREATE supports the use of a variable reference (aka @<variableName>) in its SQL forms when specifying the name of the resource instance being created.

Processed By

CREATE supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the CREATE was executed.

CREATE RULE

Since a rule is always specified as text, only a single form of the CREATE RULE statement is required.

CREATE RULE (<ruleDefiningText>) [WITH active = [true | false]]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The ruleDefiningText must be a valid rule definition starting with the keyword: RULE.

The active modifier may be included to set the initial state of the rule. A true value declares the rule to be active; a false value declares the rule to be inactive. By default rules are ACTIVE immediately after they are created.

CREATE PROCEDURE

Since a procedure is always specified as text, only a single form of the CREATE PROCEDURE statement is required.

CREATE PROCEDURE (<procedureDefiningText>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The PROCESSED BY clauses may be used to create the rule on a set of distributed nodes.

CREATE SOURCE

Object Representation
CREATE SOURCE (<sourceDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The sourceDefiningObject must be a valid source describing object. See the sources resource for the formal definition.

SQL Representation
CREATE SOURCE <sourceName>
    WITH
        type = <exprs>,
        direction = <exprs>,
        config = <exprs>,
        [state = <exprs>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

CREATE TYPE

Object Representation
CREATE TYPE (<typeDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The typeDefiningObject must be a valid type describing object. See the description of the types resource for the formal definition.

SQL Representation
CREATE TYPE <typeName>(<prop1> <type1> <typeModifers>, …)
    <indexSpecifications>
    WITH documentation = "<text>"",
        naturalKey = ["<prop1>" [, "<prop2>" ...]]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Create a new type with the specified properties. Each property is specified with a name, type and optional modifiers. Each modifier is specified as either the name of the modifier or the name of the modifier and its value. For boolean values modifiers only the name is specified and indicate that the modifier is set to true. If the modifier is not specified, it defaults to false. The modifiers are:

For decimal and currency types the scale is specified as part of the type definition. For example:

The indexSpecification contains definitions for any desired indexes in the form:

INDEX [UNIQUE][GEO] <propertyName> [ASC | DESC] {, <propertyName> [ASC | DESC]}

More than one index may be declared. If the UNIQUE modifier is included, each object in the set MUST have a unique key. If the GEO modifier is included, a single property containing a GeoJSON type must be specified as the indexed property. ASC (ascending) and DESC (descending) determines the ordering of the property values in the index. The default ordering is ascending. Additional characteristics of the type are specified in the WITH clause as <name>: <value> pairs:

naturalKey: ["prop1", ..., "<keyPropN>"]

Several attributes are specific to system types and are not available to standard automation types. They are included here for completeness:

CREATE SERIES

A series is a variant of a type that can conveniently store time series data.

Object Representation

The object representation of a series is an instance of the types resource with additional properties specified.

CREATE SERIES (<seriesDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
CREATE SERIES <name>(<propName> <type> [<typeModifiers>]
     [ = <computedPropertyExpression>], ...)
     WITH
        seriesUnits = <timeUnits>,
        startingAt = <timeOffset>,
        seriesLength = <totalLength>,
        maxCount = <totalObjectsAllowedInSeries>
        groupBy = "<propertyName>",
        window = "<propertyName>",
        documentation = "<text>"
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Create a series with the specified name. The name must be unique among all types and series. The series contains the properties specified. Of special note is that properties can be defined whose value is computed by the computedPropertyExpression. For example, the series may contain an average temperature computed for each minute from a stream of temperature readings obtained from a source. The type definition might look something like:

CREATE SERIES AvgTemp(timestamp Integer, sensorId String,
    temp Real = AVG(temp))
    WITH groupBy = ["sensorId"],
        window = "timestamp",
        seriesUnits = "minutes",
        seriesLength = 5

One of the declared properties MUST be designated as the WINDOW property containing the timestamp value for each entry in the series using the window option in the WITH clause. One property may be designated as the groupBy property containing the key for the entry in the series using the WITH clause. The window size is declared with the two properties seriesUnits and seriesLength.

By default, the start time for the series is automatically determined by the time unit declared for the window:

However, a different start can be specified. For example, the series might declare ten minute windows starting at the 3rd second of the minute. For example:

WITH startingAt = 3

For reference, the series specific properties are:

Refer to the Time Series Guide for additional details and examples.

CREATE SCALAR

Object Representation
CREATE SCALAR <scalarDefiningObject>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The scalarDefiningObject must be a valid scalar defining object. See the scalars resource for details.

SQL Representation

CREATE SCALAR <scalarName> [WITH
    baseType = <exprs>,
    comparator = <exprs>,
    toInternal = <exprs>,
    toExternal = <exprs>,
    toConstraint = <exprs>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Defines a new scalar type with the name specified.

The baseType must be a string value containg one of the built-in scalar types:

The default baseType is String.

At a minimum a string representing a procedure name must be assigned to the toInternal and toExternal options. These procedures convert the external representation of the scalar to its internal representation and vice versa. If a property of this type is expected to be used in a query constraint the toConstraint option must be assigned a procedure name that will convert the constraint on the external representation of the type value to the internal representation.

For example, assume a scalar type named feet that represents a measurement in feet. The external representation is a string of the form:

"2 feet"
"3 ft"

The internal representation of feet is as a Real value.

The toInternal option must resolve to a string representing a procedure name. The procedure will accept the string value, strip off the units designation, feet or ft, and convert the remaining string to a Real number.

The toExternal option must resolve to a string representing a procedure name. The procedure will do exactly the opposite, converting the real value to a string and then adding the unit designation, “feet”, to the resulting value.

The toConstraint option must resolve to a string representing a procedure name. The procedure will accept a constraint in object form and convert it to the required internal constraint in object form. For the example the original constraint might be:

{ "length": { "$gt": "2 feet" }}

and once converted to internal form:

{ "length": { "$gt": 2.0 }}

Scalar types are an advanced feature and should be not used without extensive experience with the rules system.

CREATE TOPIC

Create a new user defined topic.

Object Representation
CREATE TOPIC (<topicDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

See the Topic resource description for the specific content of the topicDefiningObject.

SQL Representation
CREATE TOPIC "<topicName>"
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Note that the <topicName> is enclosed in quotes. This is required because topic names are not valid identifiers since they must start with a ‘/’ character.

CREATE NODE

Create a new remote node reference. This enables distributed communication with the remote node.

Object Representation
CREATE NODE(<nodeDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

See Node resource description for the specific content of the nodeDefiningObject.

SQL Representation
CREATE NODE <nodeName> WITH <options>
    WITH
        [uri = <exprs>,]
        [username = <exprs>,] 
        [password = <exprs>,]
        [deliveryMode = <exprs>, 
        [properties = <JSONObjectContainingProperties>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Create a new node with the specified name representing a connection to the peer node specified. Node attributes are specified in the WITH clause.

CREATE CONFIGURATION

CREATE CONFIGURATION (<configurationDefiningObject>)

See the Configuration resource description for the specific content of the configurationDefiningObject.

SQL Representation
CREATE CONFIGURATION <packageName>
    WITH
        [rules = [<exprs>, ...],]
        [types = [<exprs>, ...],]
        [sources = [<exprs>, ...],]
        [scalars = [<sourceName>, ...],]
        [topics = [<sourceName>, ...],]
        [nodes = [<sourceName>, ...],]
        [state = ACTIVE | INACTIVE]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Create a new configuration with the name specified and containing the specified rules, types, sources, topics, scalars and nodes.

ALTER

The ALTER statement modifies the definition of an automation artifact. The syntax is identical to the CREATE statement so the syntax is only summarized here. See the corresponding CREATE statement for additional details.

Variable References

ALTER supports the use of a variable reference (aka @<variableName>) in its SQL forms when specifying the name of the resource instance being updated.

Processed By

ALTER supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the ALTER was executed.

ALTER RULE

ALTER RULE (<ruleDefiningText>) [WITH active = [true | false]]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

ALTER SOURCE

Object Representation
ALTER SOURCE (<sourceDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
ALTER SOURCE <sourceName>
    WITH
        type = <exprs>,
        direction = <exprs>,
        config = <JSONObjectDefinginConfiguration>,
        [state = <exprs>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

ALTER TYPE

Object Representation
ALTER TYPE (<typeDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
ALTER TYPE <typeName>(<prop1> <type1> <typeModifers>, ...)
    <indexSpecifications>
    WITH <options>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

ALTER SERIES

Object Representation
ALTER SERIES (<seriesDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
ALTER SERIES <name>(<propName> <type> [<typeModifiers>]
     [ = <computedPropertyExpression>], ...)
     WITH
        windowSize = <count> MEASUREMENTS | <duration> <timeUnits>,
        incrementSize = <intervalLength>,
        startingAt = <timeOffset>,
        seriesLength = <totalLength>,
        naturalKey = "<propertyName>",
        window = "<propertyName>"
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

ALTER SCALAR

Object Representation
ALTER SCALAR <scalarDefiningObject>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

SQL Representation

ALTER SCALAR <scalarName> [WITH
    baseType = <exprs>,
    comparator = <exprs>,
    toInternal = <exprs>,
    toExternal = <exprs>,
    toConstraint = <exprs>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

The baseType must be a string value containg one of the built-in scalar types:

The default baseType is String.

At a minimum a string representing a procedure name must be assigned to the toInternal and toExternal options. These procedures convert the external representation of the scalar to its internal representation and vice versa. If a property of this type is expected to be used in a query constraint the toConstraint option must be assigned a procedure name that will convert the constraint on the external representation of the type value to the internal representation.

For example, assume a scalar type named feet that represents a measurement in feet. The external representation is a string of the form:

"2 feet"
"3 ft"

The internal representation of feet is as a Real value.

The toInternal option must resolve to a string representing a procedure name. The procedure will accept the string value, strip off the units designation, feet or ft, and convert the remaining string to a Real number.

The toExternal option must resolve to a string representing a procedure name. The procedure will do exactly the opposite, converting the real value to a string and then adding the unit designation, “feet”, to the resulting value.

The toConstraint option must resolve to a string representing a procedure name. The procedure will accept a constraint in object form and convert it to the required internal constraint in object form. For the example the original constraint might be:

{ "length": { "$gt": "2 feet" }}

and once converted to internal form:

{ "length": { "$gt": 2.0 }}

Scalar types are an advanced feature and should be not used without extensive experience with the rules system.

ALTER TOPIC

Object Representation
ALTER TOPIC (<topicDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
ALTER TOPIC "<topicName>"
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

ALTER NODE

Object Representation
ALTER NODE(<nodeDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
ALTER NODE <nodeName> WITH <options>
    WITH
        [uri = <uri>,]
        [username = <username>,] 
        [password = <password>,]
        [deliveryMode = [bestEffort | atLeastOnce], 
        [properties = <JSONObjectContainingProperties>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

ALTER CONFIGURATION

Object Representation
ALTER CONFIGURATION (<packageDefiningObject>)
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]
SQL Representation
ALTER CONFIGURATION <packageName>
    WITH
        [rules = [<exprs>, ...],]
        [types = [<exprs>, ...],]
        [sources = [<exprs>, ...],]
        [scalars = [<exprs>, ...],]
        [topics = [<exprs>, ...],]
        [nodes = [<exprs>, ...],]
        [state = <exprs>]
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP

The drop statement is used to remove an automation artifact from the system. The specific statements are listed below.

Variable References

DROP supports the use of a variable reference (aka @<variableName>) in its SQL forms when specifying the name of the resource instance being deleted.

Processed By

DROP supports remote invocation via the standard PROCESSED BY clause. There is one result for each node on which the DROP was executed.

DROP RULE

DROP RULE <ruleName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP SOURCE

DROP SOURCE <sourceName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP TYPE

DROP TYPE <typeName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP SERIES

DROP SERIES <typeName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP SCALAR

DROP SCALAR <scalarName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP TOPIC

DROP TOPIC <topicName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP NODE

DROP NODE <nodeName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

DROP CONFIGURATION

DROP CONFIGURATION <packageName>
[[AS <resultVar>] PROCESSED BY (<queryConstraint> | ALL) 
[CONTEXT TO <contextVar>] 
[WITHIN <timeInterval>]
[UNTIL <terminatingCondition>] { 
    <statements>
}
[ON [REMOTE] ERROR (<errorVar>) {
    <statements>
}]]

Logging

It is possible to write log messages from inside rules using the predefined log variable that is available in every rule execution. The log variable holds a logger which supports the following procedures:

In all of the above cases the message formatting is done using the SLF4J MessageFormatter.

For example, given the following rule:

RULE myRule
WHEN INSERT OCCURS ON myType
log.info("An insert occurred on myType!")

The message An insert occurred on myType! will be logged if the log level is configured to be at least INFO. Log messages are written into the automation model using the logs resource. Logged messages are available via the Vantiq API or may be viewed through the Vantiq Developer’s Console. When viewing the logs in the developer’s console be aware that the log entries are first sorted on the unique invocationId associated with each execution of the rule or procedure and then sorted on the timestamp assigned to the log entry.

Log messages may be parameterized as illustrated in the following example:

RULE myRule
WHEN INSERT OCCURS ON myType
log.debug("This message contains p1: {} and p2: {}", ["first", "second"])

The message ‘This message contains p1: first and p2: second’ will be logged if the log level is configured to be at least DEBUG. Starting from the beginning of the message, each pair of braces, ‘{}’, will be replaced by the next parameter to the log invocation.

Logging levels can be set at both the namespace and rule levels, meaning it is possible to set a default, minimum logging level for the namespace, and to separately define rule-specific minimum logging levels. The default logging level for everything is INFO level. Any log messages below these levels, specifically DEBUG and TRACE messages, will not be recorded in the database. Log configurations are accessed via the loggers resource and can be updated through the Vantiq API or The Vantiq Developer’s Console.

Because INFO is the default level, Vantiq recommends that any log statements used to produce log entries during development be written as DEBUG or TRACE messages. Otherwise, the log statements will add large amounts of debug output to the production log when the rule or procedure is deployed. This will make it more difficult to filter the production log output form the unintended development diagnostic messages.

Error Handling

An exception encountered during rule evaluation will cause the rule to terminate, unless the exception is handled by an intervening TRY-CATCH block. When the rule terminates due to an exception, an ArsRuleSnapshot object is saved that describes the exception. The creation of the rule snapshot object produces an event on the ArsRuleSnapshot type that can be used as the trigger condition for another rule. This second rule can be used to handle the exception encountered in the original rule.

The general form of the exception handling rule:

RULE exceptionHandler
WHEN INSERT OCCURS ON ArsRuleSnaphot:s
<logic for handling exception>

There may be conditions under which the body of a rule will detect an error condition and wish to report the error and terminate execution of the rule. This is accomplished by invoking the exception() procedure to produce an exception, terminate rule execution and, indirectly, record that error in the ArsRuleSnapshot type. See Utility Procedures for a detailed description of the exception procedure. For example:

exception("my.exception.code", "My error message")

Rule Example

Rules can be understood be reviewing a practical example.

RULE EvaluateWeatherSituation

WHEN UPDATE OCCURS ON Weather

if (Weather.temperature < "50F" && Weather.windSpeed > "10MPH" && 
    Weather.precipitation > "0.1 inches") {

    PUBLISH { body: "Weather is poor" } TO SOURCE WeatherAdvisorySource

    UPDATE WeatherSituation(date: now(), situation: "poor") 
         WHERE location == Weather.location
}

The rule is named “EvaluateWeatherSituation” implying this rule is intended to identify a situation. The triggering condition is a change to the “Weather” type as specified in the “WHEN” clause. The object that activated the rule can be referenced via the variable: Weather. A change to Weather causes a re-evaluation of the WeatherSituation rule. The rule states that if the current temperature is less than 50 degrees F and the wind-speed is greater than 10 MPH and the precipitation is greater than 0.1 inches, the current weather situation should be updated to “poor”. The THEN clause publishes a notification to the specified source and stores the new situation in the WeatherSituation type.

Although extremely simple, this is the essence of defining a rule.

Recommendation Example

Recommendations are produced by slightly more complex rules. A recommendation is produced by applying MATCH conditions to a taxonomy (catalog) of potential recommendations to identify the recommendations that are most relevant to the current situation. The catalog is defined as a type in the model.

The taxonomy is represented by a type in the model. The type MUST define a property that contains the taxonomy category assigned to each item in the taxonomy. Typically, the taxonomy values are hierarchically organized and can use any common notation for a hierarchy as long as components of the taxonomy name proceed from most general to most specific in a left to right manner. For example:

/books/english/contemporary/mysteries

or

books.english.contemporary.mysteries

are both reasonable ways to represent taxonomy categories for books.

The recommendation process begins with the user providing a single example item from the catalog and requesting a recommendation for the most relevant recommendations given the example item provided.

The recommendation engine applies the recommendation rules to the catalog to find the most relevant recommendations.

RULE bookRecommendation

WHEN INSERT OCCURS ON Book AS b

SELECT FROM Book AS catalog WHERE category == b.category

FILTER c in catalog INTO BookRecommendations
    UNTIL BookRecommendations.size() <= 10 {
    MATCH {
        if (c.explosions >= b.explosions) {
            RECOMMEND c WITH relevance = 10
        }
    }
    MATCH {
        if (c.fights >= b.fights) {
            RECOMMEND c WITH relevance = 20
        }
    }
}

The recommendation rule applies to requests for a book that is in the category: “/books/english/contemporary/action”. If the recommendation is accompanied by a book in any other category this rule will not be activated. The catalog is represented by the Book type and the taxonomy category is represented by the property Book.category. The book name supplied as the request parameter is looked up to find the corresponding Book object in the taxonomy. It is assigned to the target “Book”.

The SELECT statement finds all other books in the category: “/books/english/contemporary/action”. The rules will now evaluate the Book supplied with the request against all other books in the category: “/books/english/contemporary/action”.

The search for recommendations then commences.

The filter statement applies the first MATCH condition to each candidate producing a new set of candidates using the recommend method. Once the rule has been applied to all candidates, the UNTIL condition is evaluated to determine if the candidate set is sufficiently refined or if more rules should be applied. If the clause evaluates to false, the next MATCH in the filter statement is evaluated. If the UNTIL clause evaluates to true, rule processing is terminated and the set of candidate recommendations are returned to the requester. This continues until the terminating condition is satisfied or until ALL MATCH statements have been applied.

In this example evaluation continues until the number of remaining candidate recommendations is less than 10 OR until both MATCH conditions have been evaluated.