Beginners in DMN often define a separate variable for every simple value in their model, a string, for example, or a number. But it’s often far better to group related values within a single variable, and the FEEL language in DMN has multiple ways to do that:
- A structure, sometimes called a context, identifies each component value with a name. For example, the structure Customer could have components Name, Birth Date, Tax ID, etc. The variable Customer references all its components; to select just one you use the dot notation, such as Customer.Name.
- A list variable contains a sequence of values, usually of the same type. List items are not restricted to simple types, but could be structures or even nested lists. Each list item is identified not by name but by its position in the list. For example, myList[1] means the first item in myList; myList[-1] means the last item. In FEEL, lists may contain duplicate values and the order of items is significant, so the list [1,2,3] does NOT equal the list [3,2,1] or the list [1,1,2,3].
- A table is a list of structures. Each row of the table is a structure of the same type. The structure components name the columns of the table. So, for example, CustomerList.Name[3] returns the Name of the third Customer in CustomerList.
Defining DMN variables as structures, lists, and tables rather than exclusively simple types usually makes your models simpler, more powerful, and easier to maintain.
Datatypes
Trisotech Decision Modeler makes it easy to define structured datatypes. You simply provide a name and type for each component, as you see here:

There are many ways to create a list. The simplest is the list operator, square brackets enclosing a comma-separated list of expressions, such as [1,2,3]. The datatype of a list variable is called a collection, and best practice is to name the type Collection of [item type]. For example, the datatype of a table of IncomeItems with columns IncomeType and MonthlyAmount would be specified like this:

List Functions
FEEL provides a wide variety of built-in functions that operate of lists and tables. Some are specific to lists of numbers and date/times, but most apply to any list item type. Examples are shown in the table below:

Filters
To select an item from a list or table, FEEL provides the filter operator, modeled as square brackets immediately following the list or table name, containing a filter expression, either an integer or a Boolean. If an integer, the filter returns the item in that position. For example, myList[1] returns the first item in myList. A Boolean expression returns all items for which the expression is true. For example, Loan Table[ratePct<4.5] returns the collection of all Loan Table items for which the component ratePct is less than 4.5.
A Boolean filter always returns a collection, even if you know it can return at most a single item. If each Customer has a unique id, Customer[id=123456] will return a list containing one Customer, and Customer[id=123456].Name returns a list containing one Name. A list containing one item is NOT the same as the item on its own. It is best practice, then, to append the filter [1] to extract the item value, making the returned value type Text rather than Collection of Text: Customer[id=123456].Name[1].
It is quite common that a filtered list or table is the argument of a list function, for example, count(Loan Table[ratePct<4.5]) returns the count of rows of Loan Table for which ratePct is less than 4.5.
If a Boolean filter expression finds no items for which the expression is true, the returned value is the empty list [], and if you try to extract a value from it you get null. For example, if the table Customer has no entry with id=123456, then Customer[id=123456] is [] and Customer[id=123456].Name[1] is null. Because of situations like this, DMN recently introduced B-FEEL, a dialect of FEEL that better handles null values, and you should be using B-FEEL in your models. Without B-FEEL, concatenating a null string value with other text returns null; with B-FEEL the null value is replaced by the empty string “”.
Iteration
A powerful feature of FEEL is the ability to iterate some expression over all items in a list or table. The syntax is
for [range variable] in [list name] return [output expression]
Here range variable is a name you select to stand for a single item value in the list. For example, if LoanTable is a list of mortgage loan products specified by lenderName, rate, pointsPct, fees, and term, we can iterate the amortization formula over each loan product to get the monthly payment for each loan product:
for x in LoanTable return payment(RequestedAmt*(1+x.pointsPct/100), x.rate/100, x.term)
Here x is the range variable, meaning a single row of LoanTable, and payment is a BKM containing the amortization formula based on the loan amount, loan rate, and term to get the monthly mortgage payment. To simplify entry of this expression, Trisotech provides an iterator boxed expression.
In addition to this iteration over item value, FEEL provides iteration over item position, with the syntax
for [range variable] in [integer range] return [output expression]
For example,
for i in 1..10 return i*i
returns a list of the square of integers from 1 to 10.
Testing Membership in a List
FEEL provides a number of ways to check whether a value is contained in some list.
some x in [list expression] satisfies [Boolean expression]
returns true if any list item satisfies the Boolean test, and
every x in [list expression] satisfies [Boolean expression]
returns true only if all list items satisfy the test.
The in operator is another way of testing membership in a list:
[item value] in [list expression]
returns true if the value is in the list.
The list contains() function does the same thing:
list contains([list expression], [value])
And you can also use the count() function on a filter, like this:
count([filtered list expression])>0
Set Operations
The membership tests described above are ways to check whether a single value is contained in a list, but sometimes we have two lists and we want to know whether some or all items in listA are contained in listB. Sometimes we want to consider comparing setA to setB, where these sets are deduplicated and unordered versions of the lists. If necessary, we can use the function distinct values() to remove duplicates from the lists.
Intersection
ListA and listB intersect if any value is common between them. To test intersection, we can use the expression
some x in listA satisfies list contains(listB, x)
Containment
To test whether all items in listA are contained in listB we can use the every operator:
every x in listA satisfies list contains (listB, x)
Identity
The simple function listA=listB returns true only if the lists are identical at every position value. Testing whether the deduplicated unordered sets are identical is a little trickier. We can use the FEEL union() function to concatenate and deduplicate the lists, in combination with the every operator:
every x in union(listA, listB) satisfies (list contains(listA, x) and list contains(listB,x))
Sorting a List
We can sort a list using the FEEL sort() function, which is unusual in that the second parameter of the function is itself a function. Usually it is defined inline as an anonymous function, using the keyword function with two range variables standing for any two items in the list, and a Boolean expression of those range variables. The Boolean expression determines which range variable value precedes the other in the sorted list. For example,
sort(LoanTable, function(x,y) x.rate<y.rate)
sorts the list LoanTable in increasing value of the loan rate. Here the range variables x and y stand for two rows of LoanTable, and row x precedes row y in the sorted list if its rate value is lower.
Replacing a List Item
Finally, we can replace a list item with another using Trisotech’s list replace() function, another one that takes a function as a parameter. The syntax is
list replace([list], [match function], [newItem])
The match function is a Boolean test involving a range variable standing for some item in the original list. For example, EmployeeTable lists each employee’s available vacation days. When the employee takes a vacation, the days are deducted from the previous AvailableDays to generate an updated row NewRecord. We use the match function to select the employee matching the EmployeeId in another input, VacationRequest.

Now we want to replace the employee record in that table with NewRecord:
list replace(EmployeeTable, function(x, Vacation Request) x.employeeid=VacationRequest.employeeId, NewRecord)
Here the range variable x stands for some row of EmployeeTable, and we are going to replace the row matching the employeeid in VacationRequest. So in this case we are replacing a table row with another table row, but we could use list replace() to replace a value in a simple list as well.
The Bottom Line
In real-world decision logic, your source data is often specified using lists and tables. Rather than extracting individual items in your input data and processing one item at a time, it is generally easier and faster to process all the items at once using lists and tables. FEEL has a wide variety of functions and operators that make it easy, and the Automation features of Trisotech Decision Modeler make it easy to execute them.
Want a deeper dive into the use of lists and tables? Check out our DMN Method and Style training.
Leave a Reply