You’re a business analyst, not a software engineer, and you’ve been asked to help implement parts of a new BPMN or DMN model for your business. Dragging and dropping the BPMN elements is pretty easy. So is adding labels. But then you need to write a small piece of code to make an exclusive gateway follow the right path. You’re not a programmer! How are you supposed to do that?!
Have no fear! FEEL is here! FEEL, or Friendly Enough Expression Language, lives up to its name: it is simple to get started with and understand, but powerful enough that you can write an expression for almost any data operation you need to perform.
What is FEEL, exactly?
FEEL is part of the DMN specification from the Object Management Group (OMG). FEEL is not a fully featured programming language. It is meant to create and execute “expressions”: generally speaking, an expression is a series of commands that produce a value. For instance, 1 + 1
is an expression that produces the value of 2
. (Of course, in programming expressions are often more complex than this, but this is a usable definition for FEEL.) This means you can do some pretty complex data manipulation, but you can’t write a complete program using just FEEL.
One important feature of FEEL is that it uses “readable” language that feels more like writing a sentence than a computer instruction. Code often looks unintelligible (sometimes even to a veteran software developer!) with the use of lots of symbols (like dots, colons, asterisks, brackets, braces, and more). Different programming languages often have their own idiomatic way of writing code; being able to read one programming language doesn’t necessarily mean you can read another.
FEEL attempts to make the code feel less like “code” and more like “language.” Let’s look at a quick example to illustrate what I mean. Let’s say you want to transform a word to all uppercase letters. In a programming language like Java, you would write:
“hello”.toUpperCase();
In FEEL:
= upper case(“hello”)
There are similarities between the two. For instance, both words are surrounded by double quotes (more on that next!), but the FEEL syntax feels more like something you would say out loud.
Note: In a BPMN model or Camunda Form, FEEL expressions begin with an equals sign (“=”); in DMN, you don’t need the equals sign!
Understanding data in FEEL
In FEEL, every value has a type. This helps the computer understand what the value is, and allows us to perform evaluations with the value. There are several types supported by FEEL:
- Strings: these are alpha-numeric values. String values are surrounded by double quotes, and can contain any character between the quotes.
- Numbers: these are, as the name implies, only numbers. Number values are not surrounded by quotes. (
100
is a number, but”100”
is a string!) FEEL supports both whole numbers as well as decimals. - True/False: called “boolean” values, you can represent a true or false value in FEEL. As with numbers, a boolean value does not have quotes. (
true
is a boolean, but”true”
is a string!) - Lists: sometimes referred to as arrays, a list is what it sounds like: a list of values. The values can be of any type, so you can mix and match values as needed. Lists are a set of comma-separated values inside of brackets:
[ 1, “one”, true ]
is a list of three items. You can also nest the data and have lists inside of lists! ([ [“list one”], “inside” “list two” ]
) - Contexts: sometimes referred to as objects, contexts are a collection of “key-value pairs” surrounded by braces. The key is a string, and the value can be of any type (including a list or another context):
{ “keyA”: “value A”, “keyB”: 123, “listKey”: [ “contains”, “a”, “list” ] }
FEEL also supports date, time, and duration values. I won’t be covering those in this post to keep things a little simpler; if you need to work with date/time values, please read our FEEL documentation and ask any questions you have on the forum!
Data in FEEL is usually assigned to a variable, allowing the data to be referenced more easily in other parts of the process. Variables are names that contain a value. For instance, when you get a response back from the REST Connector, it is a variable named response
that contains a context value.
Working with data
Now that we have a basic understanding of what data looks like with FEEL, it’s time to learn how to work with that data. There are many different operations you can perform with FEEL, and they can be categorized as either “expressions” or “functions.” (That’s how you’ll find these categorized in our documentation. I know it’s a bit confusing using “expression” in two different contexts, but it will make sense in the end!) Generally speaking, the expressions use the value (for instance, performing arithmetic on numbers or getting a value out of a list) and usually work with a single value, while the functions manipulate the value (for instance, converting a string to a number or merging two contexts) and usually work with multiple values. (Note: these are not strict definitions, but I find it helpful to think of them this way.)
Let’s use some real data to learn how these expressions and functions can be used together. If you find yourself feeling a bit lost at any point, I recommend referring back to the documentation then coming back to the example. (If you prefer video training, Camunda Academy has an excellent FEEL video series you can watch too!) For the real data, I’m going to use a fun data set from the Marvel API (yes, a data set about comics)! This data set contains information about the character Spider-Man and related comics, stories, and artists. Don’t worry, you don’t have to use the API for this; you can look at the data set for these examples by clicking here. After fetching the details for a single character, we need to transform the data into the following context shape:
{
“name”: string,
“description”: string,
“numberOfAvailableComics”: number,
“topCreators”: string,
“eventSummary”: list of contexts [ {
“eventName”: string,
“endedOrContinuing”: string
} ]
}
First, we get the name, description, and number of comics. These are elements directly in the data set, so all we need to do is reference the appropriate variable name. The data from the API comes as a JSON object, which is automatically converted to a FEEL context in Camunda. You can access individual properties of the context using a dot (.
):
= {
“name”: data.name,
“description”: data.description,
“numberOfAvailableComics”: data.comics.available
}
(Try it in the FEEL Playground!)
Next, we will fill in our list of creators. I want this to be a comma separated list with the names of the available creators. Looking in the data set, there are lists of creators available for each event. There are quite a few names available, so I decided I want to filter the list to just those with the role of “writer”, and select only up to the first five. Let’s see how we might do that in FEEL:
{
"combinedListOfCreators": flatten(data.events.items.creators.items),
"onlyWriters": combinedListOfCreators[item.role = "writer"],
"onlyDistinctWriters": distinct values(onlyWriters),
"limit5Writers": sublist(onlyDistinctWriters, 1, 5),
"topCreators": string join(limit5Writers.name, ", ")
}
(Try it in the FEEL Playground! Scroll to the bottom of the output to see the top creators.)
Whoa, that looks complicated! It might look that way, but it makes sense once you break it down into its component parts. Let’s do that!
There are two events in the data set, each with a list of creators; the first step is to merge those two lists so that we have a single list to work from: flatten(data.events.items.creators.items)
. The “flatten” function turns multiple lists—including nested lists—into a single list. Doing it this way, we don’t need to know how many items there are in the list. The FEEL engine automatically discovers every nested list and flattens them all. If there were seven events instead of two, we would get the same single list result with the flatten
function. This is one of the wonderful features of FEEL! We store this in a new variable named combinedListOfCreators
.
I decided I only want to list creators with the role of “writer”, so the next step is to filter our combined list. To filter a list in FEEL, you place a filter condition inside of brackets. For instance, the expression =listOfNumbers[item > 10]
will only return items that are greater than 10. The filter condition always has access to a variable named item
which represents one item in the list. In our combined list, each item has a role
property that we can filter on: =combinedListOfCreators[item.role = “writer”]
. Now we have a variable named onlyWriters
that contains only creators with the role of “writer”.
Of course, it’s possible that there are duplicate names between the two events! To ensure we don’t have duplicate names in our list, we can use the “distinct values” function to ensure only unique items exist in the list, and assign it to a new variable named onlyDistinctWriters
: distinct values(onlyWriters)
I chose to limit the list to five names only, because there are lots of names available in the data. We can use the “sublist” function to create a list of a specific length (in our case, a length of 5): sublist(onlyDistinctWriters, 1, 5)
. What do the numbers mean? We want our sublist to start with the first writer and contain up to 5 names. (If we had a different data set and we needed to get the 10th through 20th results, we could use sublist(listName, 10, 10)
!)
Last, I wanted to show the names a comma separated list. We can use the “string join” function to take each item in the list and make a single string out of it. Using the second parameter in the function, we can provide what the delimiter is. In this case, we want it to be a comma: string join(limit5Writers, “, “)
(don’t forget the space after the comma!).
Now that we walked through that entire example, have a look at this FEEL expression:
= {
"topCreators": string join(
sublist(
distinct values(
flatten(data.events.items.creators.items)[item.role = "writer"]
), 1, 5).name, ", "
)
}
(Try it in the FEEL Playground!)
This FEEL expression does the exact same thing as the one above, but it doesn’t use the intermediate variable names. FEEL can work both ways, and how you use it depends entirely on what you need to accomplish, and how comfortable you are working with FEEL!
One more example!
Now that we’ve gone through one example step by step, take a look at this next FEEL expression. See if you can figure out how it works before we walk through it:
{
"topEvents": for event in data.events.items return {
"eventName": event.title,
"endedOrContinuing": if event.end != null then "Ended" else "Continuing"
}
}
(Try it in the FEEL Playground!)
The last features of FEEL I’m going to cover in this post are conditionals, loops, and nulls. A conditional is an “if” statement: if something is true, then this, or else that. In FEEL, a conditional statement starts with the keyword “if”; the condition following “if” must have a true or false result. In other words, if 5 + 10
is not a valid condition, because “5 + 10” isn’t true or false. The example condition is first checking if the end
property has a value (more on that next), and if it does, then it sets endedOrContinuing
to the string value “Ended”; if the end
property doesn’t have a value, then it sets endedOrContinuing
to the string value “Continuing”.
The Marvel API returns anend
property if the event or story has ended; if the event or story is continuing, there is no end
property returned from the API. You check for a missing variable by checking if it is null
. Sometimes, a variable can exist and also be set to a value of null
. Either way, there is no value, so for both conditions, the FEEL engine returns true. (Try it in the FEEL Playground!)
Finally we have the loop. There are many cases where you need to go through each entry in a list and do something with that entry. In FEEL, you do this with the for
loop: for each entry in a list, return something. In the example, we are looping through every entry in the list of events, and returning a new context that includes the name (or title) of the event and whether it has ended or is continuing. Just like in the previous example, you can combine multiple FEEL expressions and functions together into one single expression, or you can separate them into intermediate variables.
Bringing it all together
Let’s take a look at the final FEEL expression:
= {
“name”: data.name,
“description”: data.description,
“numberOfAvailableComics”: data.comics.available,
"topCreators": string join(
sublist(
distinct values(
flatten(data.events.items.creators.items)[item.role = "writer"]
), 1, 5).name, ", "
),
"topEvents": for event in data.events.items return {
"eventName": event.title,
"endedOrContinuing": if event.end != null then "Ended" else "Continuing"
}
}
(Try it in the FEEL Playground!)
Congratulations, you made it to the end! You now know more about FEEL than you did just a few minutes ago. If you came into this post with no FEEL experience, I don’t expect you to now be an expert. My hope is that you are more comfortable getting started.
This post doesn’t cover everything FEEL can do. Not only are there additional data types that represent dates, times, and durations, but there are also a lot of additional expressions and functions for each data type too! So where next?
- As always, the Camunda FEEL documentation is a great place to start. The docs cover all the available data types, expressions, and functions.
- Camunda Academy has a wonderful FEEL video series that provides examples and exercises in addition to describing the different features of FEEL.
- Find yourself having trouble getting your expression to work correctly? Join the Camunda community forum and ask your question there. Don’t forget to search, too!
There are some exciting new features coming to Camunda’s FEEL capabilities in the future too. In recent releases, we’ve improved the auto-complete in Camunda Modeler to better help with FEEL syntax. Keep an eye on future release announcements for some exciting new FEEL features too.
Happy coding!
Start the discussion at forum.camunda.io