Scripting language

Scripting language of the BRE

Scripting language reference

This page is the language reference for the BRE (business rule engine) scripting language used throughout the Vertex platform. It covers the statement model, the data types, the operators and the control flow constructs, with worked examples for the pitfalls new rule authors run into. Every code example on this page is executable as written and is pinned by a docs_* script case in EE.Library.UnitTests\Rules\ScriptCases, so the documented behavior is the tested behavior.

Working in the script editor

  • On the Vertex home page press the button “script library” to access all the scripts that have currently been made.
  • Then press the button “new script” to create a new script.
  • Select a name and potentially also a description for your script and press on “create and add properties”. A text editor will open where you can edit your script.

A context can be entered into the script by pressing the “import context” button on the top right of the script text editor. This context provides variables that can be accessed from within the script. All the available variables for a certain context can be viewed by importing that context and opening the “input” tab under the text editor. Pressing the green “run” button runs the script and fills the output tab underneath the editor.

Statements

A script is a sequence of statements. Each statement is an expression; an assignment is an expression too. When the script finishes, the value of the last evaluated statement is the result of the script (next to whatever was explicitly assigned to the output, see Variables and assignment).

parameterName1 = 1
parameterName1 + 1

Result: 2. The first line stores a value under the name parameterName1, the second line reads it back and produces the script result.

Statement separators: newline or ;;

Statements are separated by a line break, by the ;; separator, or a mix of both. These two scripts are identical:

price = 10
quantity = 4
price * quantity
price = 10;; quantity = 4;; price * quantity

Result of both: 40. Use ;; when statements must share a line, for example inside a single-line block. A single ; does NOT separate statements - it separates arguments and elements, which is the next topic.

One character apart: ; vs ;; vs : vs ,

Four separators look alike but mean very different things. Getting these wrong is the most common beginner mistake in this language.

Separator Meaning
; Separates method arguments, collection elements and object properties: pow(10; 2), c[1; 2], {A: 1; B: 2}
;; Separates statements on one line: a = 1;; b = 2
: Separates an object property key from its value: {Price: 10}
, A decimal separator inside a number - NEVER an argument separator

The comma deserves special attention. 1,5 is one single number token, so a call like this does not do what it appears to do:

round(1,5)

This is a call to round with ONE argument, not two. The engine reads 1,5 as a single number: in a rule context whose input culture uses the comma as decimal separator that number is 1.5 (so this parses as round(1.5)), and in a point-decimal culture such as en-US the comma is a thousands separator and the number is 15. Either way round then fails at run time, because it requires two arguments (the value and the number of decimals). Arguments are always separated with ;:

round(1.5; 0)

Result: 2.

Both decimal separators are accepted

The grammar accepts both . and , inside a number literal. Which one denotes the decimal fraction is decided by the input culture of the rules context the script runs in: 1.5 is one and a half under en-US but fifteen under nl-NL (where . groups thousands), and vice versa for 1,5. Write number literals with the decimal separator of the context your script runs in, and keep the previous section in mind: a comma never separates arguments.

Data types

Numbers

All numeric values share one number type. Standard arithmetic works as expected, including scientific notation and repeated signs:

0.5+0.5
((1+1)+1)
--1
1e10
10/2
10^2

The lines evaluate to 1, 3, 1, 10000000000, 5 and 100 (the last line is the script result). ^ is the power operator and --1 resolves to 1 because the two unary minus signs cancel out.

Booleans

TRUE and FALSE (in any casing, see Case-insensitivity) form the boolean type. Booleans are typically produced by the relational operators:

1 > 0
10 <= 10
2 != 2

The lines evaluate to true, true and false. Booleans combine with the logical operators:

NOT true
true AND false
true OR false

The lines evaluate to false, false and true. Boolean literals are case-insensitive:

(true == TRUE) AND (False == FALSE)

Result: TRUE.

null

null represents the absence of a value and is case-insensitive as well:

IsNull(null) AND IsNull(NULL) AND IsNull(NuLl)

Result: TRUE. Use IsNull to test for it; numeric() without arguments produces a null number:

IsNull(numeric())
IsNull(36)

The lines evaluate to true and false.

Strings

A string literal is text between single or double quotes:

"This is a string"

To put a quote of the surrounding style inside the literal, escape it with a forward slash: /' produces ' and /" produces ":

single = 'It/'s quoted'
double = "Say /"hi/""
single + ' - ' + double

Result: It's quoted - Say "hi". Strings are concatenated with +. Note that the escape character is the forward slash /, not the backslash used in many other languages.

Date and time

A datetime value represents a specific date and time. Construct one with the DateTime method from a string in dd-MM-yyyy, dd/MM/yyyy or ISO yyyy-MM-ddTHH:mm:ss format:

DateTime('01-01-2017')
DateTime('2009-06-15T13:45:30')

The first line is midnight on 1 January 2017, the second is 15 June 2009 at 13:45:30. How a datetime is displayed depends on the output culture of the rules context.

Collections

A collection is an ordered list of values, written as c[ … ] with ; between the elements. Elements can have mixed types and can be collections themselves:

c[true;false;3;c[1;2];"hi"]

The IN and NOTIN operators test membership:

true IN c[false;true;false;true;1;2;3;'1';'hi1';'hi2']
true NOTIN c[false;true;false;true;1;2;3;'1';'hi1';'hi2']

The lines evaluate to true and false. Elements are retrieved by zero-based index with elementat:

elementat(c[10;100;1000;10000;100000];0)

Result: 10.

Objects

An object stores key-value pairs. An object literal is written between braces, with : between key and value and ; between the properties:

ThisIsAnObject = {FirstKey: c[3;true;"Hi"]}

Properties are read with the dot notation, see Property access:

order = {Price: 10; Quantity: 2}
order.Price * order.Quantity

Result: 20. An empty object is created with object(), and properties can be added or replaced through the dynamic accessor .[key], which also works with computed keys:

ThisIsAnObject = object()
ThisIsAnObject.["FirstKey"] = c[3;true;"Hi"]
ThisIsAnObject.["FirstKey"]

Result: c[3; TRUE; 'Hi'].

Custom enums

A custom enum value identifies one entry of a tenant-defined enumeration. Enum values usually arrive through the input context or decision tables; the CustomEnum(enumComponentId; valueComponentId) method constructs one explicitly from the component ids. Two enum values are equal when they refer to the same enumeration entry:

status = CustomEnum('07A3EA2F-E080-4D7C-8CDD-1150702D407F'; '8CDD4A12-2A38-4F99-A004-8BBCA8FCE65F')
status == CustomEnum('07A3EA2F-E080-4D7C-8CDD-1150702D407F'; '8CDD4A12-2A38-4F99-A004-8BBCA8FCE65F')

Result: TRUE.

Variables and assignment

The language has two assignment operators, and the difference matters:

  • = assigns to a LOCAL variable. Locals only exist while the script runs and are discarded afterwards.
  • <- assigns to the OUTPUT of the script. Everything assigned with <- is returned to the caller of the script.
parameterName1 = 1
parameterName1 + 1

Result: 2, and the output channel stays empty because parameterName1 was only a local.

outputVariable <- 3
outputVariable - 2

Result: 1, and the script output additionally contains outputVariable with value 3. Output variables can be read back in the script just like locals. Variables that come from the imported context (the input) can be read but never assigned.

Variable names consist of letters, digits and underscores, and are case-insensitive: myValue and MYVALUE are the same variable.

Operators

Category Operators
Arithmetic + - * / ^ (power), unicode alternatives × · for * and ÷ for /
Comparison == != > >= < <= (=> and =< are accepted alternative spellings of >= and <=)
Logical AND or &&, OR or `
Membership IN, NOTIN
Assignment = (local), <- (output)

Multiplication and division bind stronger than addition and subtraction, and ^ binds stronger still; use parentheses whenever the intent is not obvious at a glance. Note that a single = between two values is an assignment, never a comparison - equality is always ==.

Property access

Values with named members (objects, context inputs) are accessed with a dot:

inputPricing.totalValue < 100000

The dot and the member name must be written together: a.b is member access, but a . b is a syntax error because a free-standing . is read as a decimal separator.

When the key is not a valid name or is computed at run time, use the dynamic accessor .[expression]:

ThisIsAnObject = object()
ThisIsAnObject.["FirstKey"] = c[3;true;"Hi"]
ThisIsAnObject.["FirstKey"]

The dynamic accessor is also the only member form that can be assigned to.

Lambda arguments

Several collection methods take a small inline function - a lambda - as an argument. A lambda is written as variable $ expression: the variable is bound to each element in turn and the expression is evaluated for it.

any(c['hi';'hello';'dag';'test';'nn']; x $ x == 'test')
any(c['hi';'hello';'dag';'test';'nn']; x $ x != 'test')

Both lines evaluate to true (any asks whether at least one element matches).

all(c['hi';'hello';'dag';'test';'nn']; x $ IsString(x))
all(c['hi';'hello';'dag';'test';'nn']; x $ x == 'thisisnotonthelist')

The lines evaluate to true and false (all asks whether every element matches). Other lambda-taking methods such as select and groupby follow the same x $ expression shape.

Comments

Two comment forms exist, and both are completely invisible to the engine:

  • // comments out the rest of the line.
  • /* … */ comments out everything in between, may span multiple lines, and may even sit in the middle of an expression.
// the base price
price = 10 // a trailing comment
total = /* tax */ price + 5
/* block comments
   can span lines */
total

Result: 15. Comment markers inside a string literal keep their literal meaning: 'http://example.com' is just a string. A /* must always be closed with */, otherwise the script has a syntax error.

Control flow

IF / ELSEIF / ELSE

The conditional is an expression: IF condition THEN result ELSEIF condition THEN result ELSE result. It produces the value of the chosen branch:

(IF 16 > 2*4 THEN 'HALLO' ELSE 'DOEG')

Result: 'HALLO'.

(IF 1==2 THEN 10 ELSEIF 200!=200 THEN 100 ELSEIF 30+5>30 THEN 1000 ELSE 10000)

Result: 1000. Conditions are checked top to bottom and the first true branch wins.

The braceless IF pitfall

IF has no braces and no end marker. A branch is a full statement list, so an unparenthesized IF absorbs the FOLLOWING statement lines into its branch. This script looks like it always assigns 'after the if':

IF 1 > 2 THEN
    x = 'from the branch'
x = 'after the if'
x

It does not. Both assignment lines belong to the THEN branch, the condition is false, so neither runs and the script fails with Could not find 'x' in the context. The indentation suggests a structure the language does not have.

The fix: wrap the conditional in parentheses. The closing ) unambiguously ends the IF, and everything after it is ordinary top-level script again:

y = (IF 1 > 2 THEN 'branch' ELSE 'fallback')
x = 'after the if'
y + ' ' + x

Result: 'fallback after the if'. Rule of thumb: when any statement follows an IF on later lines, parenthesize the IF.

WHILE

WHILE(condition) { body } evaluates the condition BEFORE every iteration and runs the body while it is true. The value of the loop is the value the body produced in its last iteration:

a <- 0
while(a < 5)
{
    a <- a + 1
}

Result: 5.

To protect against endless loops the engine stops a WHILE after 10000 iterations by default and raises the error Maximum number of iterations ('10000') reached. A loop that legitimately needs a different bound passes it as the second argument, separated - as always - with ;:

i = 0
while(i < 3; 10)
{
    i = i + 1
}
i

Result: 3 (the loop ran three times, well within its explicit bound of ten). The bound argument raises or lowers only the safety limit; it never changes how often the condition is checked.

FOREACH

FOREACH(variable IN collection) { body } runs the body once for every element, with the element bound to the variable:

number = 0
foreach(x in c[1;2;3]){
number = x + number
}
number

Result: 6.

SEQUENCE

SEQUENCE { statements } groups a statement list into one expression whose value is the last statement’s value. Use it where a single expression is expected but several steps are needed:

sequence {
    subtotal = 40
    subtotal * 1.21
}

Result: 48.40. Note that the { must be on the same line as the sequence keyword; a line break between them is a syntax error (unlike WHILE and FOREACH, which do allow the brace on the next line).

Case-insensitivity

The language is case-insensitive across the board:

  • Keywords: if/IF, then/THEN, while/WHILE, foreach/FOREACH, sequence/SEQUENCE, and/AND, or/OR, xor/XOR, not/NOT, in/IN, notin/NOTIN - any casing works.
  • The literals TRUE, FALSE and null.
  • Method names.
  • Variable names.
(if TRUE then 'yes' else 'no') == (IF true THEN 'yes' ELSE 'no')

Result: TRUE.

ROUND(2.5; 0) == round(2.5; 0)

Result: TRUE. Pick one casing convention and stick to it for readability - the engine does not care, but your colleagues do.

Built-in methods

The sections below show commonly used built-in methods per data type with examples. Methods are called as name(argument; argument; ...) and their names are case-insensitive.

Number methods

SyntaxExplanationScriptOutput
Log(N)Returns the logarithm base 10 of the number N
log(10)
2.30258509299405
exp(N)Returns the number N to the power of e - Euler's constant
exp(1 / 365)
1.00274348250654
sqrt(N)Returns the square root of the specified number N
sqrt(2^10)
32
pow(N1;N2)Returns N1 to the power of N2
pow(10;2)
100
mod(N1;N2)Returns N1 modN2, i.e. the remainder of N1/N2
mod(3;3)
0
remainder(N1;N2)Returns N1 modN2, i.e. the remainder of N1/N2
remainder(5;3)
2

Boolean methods

SyntaxExplanationScriptOutput
ToBoolean(N)Returns true when N is larger than 0 and false when N is 0 or smaller.
ToBoolean(1)
true
IsNull(A)Returns true if A is of type null
IsNull(numeric())
IsNull(36)
true
false

Collection methods

SyntaxExplanationExample ScriptExample Output
sum(C)Sums all the elements in the collection
sum(c[1;2;3;4;5])
15
max(C)Returns the maximum element in the collection
max(c[1;2;3;4;5])
5
min(C)Returns the minimal element in the collection
min(c[1;2;3;4;5])
1
count(C)Counts the number of elements in the collection
count(c[1;2;3;4;5])
5
avg(C)Calculates the average of all the elements in the collection
avg(c[1;2;3;4;5])
3
average(C)Calculates the average of all the elements in the collection
average(c[1;2;3;4;5])
3
wavg(C1;C2)Outputs the average of C1 where the elements are weighted by C2
wavg(c[10;25;15;40;10];c[10;100;1000;10000;100000])
14176
elementat(C; N)Outputs the element at index N in collection C
elementat(c[10;100;1000;10000;100000];0)
10
take(C; N)Returns the first N elements of the collection C into a new collection
take(c['hi';'hello';'dag';'test';'nn']; 2)
c['hi';'hello']
skip(C; N)Returns a collection of the elements of C after the Nth index
skip(c['hi';'hello';'dag';'test';'nn']; 3)
c['test';'nn']
additem(C; A)Returns a collection which is the original collection C appended by the new element A to its end
additem(c['hi';'hello';'dag';'test';'nn']; 3)
c['hi';'hello';'dag';'test';'nn'; 3]
any(C; x $ A(x))x Loops over all elements in C, returns true if any of the variables satisfy the condition A, which includes x
any(c['hi';'hello';'dag';'test';'nn']; x $ x == 'test')
any(c['hi';'hello';'dag';'test';'nn']; x $ x != 'test')
true
true
all(C; x $ A(x))x loops over all elements in C, returns true if all elements of C satisfy the condition A, which includes x
all(c['hi';'hello';'dag';'test';'nn']; x $ IsString(x))
all(c['hi';'hello';'dag';'test';'nn']; x $ x == 'thisisnotonthelist')
true
false

Datetime methods

SyntaxExplanationExample ScriptExample Output
DateTime(S)

Generates a datetime object. The specific date and time are specified by the string S. S can have multiple formats:

  • dd-MM-yyyy
  • dd/MM/yyyy
  • yyyy-MM-ddTHH:mm:ss
DateTime('01-01-2017')
DateTime('2009-06-15T13:45:30')
01-01-2017 00:00:00
15-06-2009 13:45:30

Time series methods

The BRE can be used for the retrieval and manipulation of time series from Vertex.

SyntaxExplanationExample ScriptExample Output
tsselector(A; B; C; D; E)

Selector which refers to a specific time series in Vertex by its time series code (A), data slot reference (B), revision (C), ..., ... . Note that the last three arguments are optional.

tsselector('ME20191101009'; 'SalesPortfolio_ME20191101009:P_PFC_VOL_ZERO')

getts(A; B; C)
getts()

Other methods

SyntaxExplanationExample ScriptExample Output
format(A; B)Formats input A as specified by the string B. Note that for some cases, the formatting is also influenced by the context, such as a geolocation.
Format(DateTime('02/01/2015');'dd-MM-yyyy')



02-01-2015

exists(A)Returns true if A is some object that has been defined within the code or has been predefined as a context or global variable or object.
exists(ThisVariableWasNotDefined)
exists(0)
exists(null)
false
true
true