FEEL Overview
Friendly Enough Expression Language (FEEL) is the standardised expression language defined by the OMG DMN specification. It is designed to be executable while remaining readable for business users, and is the language QuantumBPM uses for every dynamic value in both DMN decisions and BPMN processes.
💡 Try FEEL in the browser at feel-playground.quantumbpm.com - the same LSP server that runs in the modeler powers an in-browser editor with autocomplete, hover docs, signature help, and live diagnostics. Every expression you write gets a shareable URL, so you can send a colleague a link to a reproduction or a snippet to review without any setup on their end.
Introduction​
FEEL provides a standard syntax for expressing decision and routing logic. Whether you are defining a complex decision-table rule, a simple literal calculation, or a sequence-flow condition, you are likely using FEEL. Its syntax draws inspiration from Java, JavaScript, and Excel formulas but is optimised for declarative business logic.
Key Characteristics​
- Side-Effect Free: FEEL expressions are purely functional. They calculate a value based on inputs and return a result without interacting with external systems or changing state. This makes FEEL safe to re-evaluate during workflow replay.
- Rich Data Types: Built-in support for numbers, strings, booleans, dates, times, durations, lists, and contexts (key-value objects).
- Three-Valued Logic: Logic operations like
and/orhandletrue,false, andnull(unknown), ensuring robust handling of missing data. - Type-Strict: No implicit conversions - the number
123is not equal to the string"123". Conversions are always explicit.
Where is FEEL Used?​
FEEL is used in two distinct places in QuantumBPM:
In DMN​
- Decision Tables - input entries (unary tests) and output entries.
- Literal Expressions - the core logic of a decision node.
- Boxed Expressions - context entries, list definitions, function invocations, and relations.
- Input Data - defining constraints or default values.
In BPMN​
- Sequence-flow conditions (
<conditionExpression>) - gateway routing. - I/O mappings (
<quantum:ioMapping>source expressions on every task, sub-process, and call activity). - Script tasks - the entire body, with
scriptFormat="feel". - Timer expressions -
timeDuration,timeDate,timeCycleon timer events and timer boundaries. - Multi-instance loop characteristics -
loopCardinality,inputCollection,outputElement,completionCondition. - Standard loop characteristics -
loopCondition,loopMaximum. - Ad-hoc sub-processes -
activeElementsCollectionandcompletionCondition. - Message correlation -
<quantum:subscription correlationKey>on message events and receive tasks. - Conditional events - the
<condition>ofconditionalEventDefinition. - Business-rule task inputs - when invoking a DMN decision.
- User-task assignment fields - assignees, candidate groups, candidate users.
In short: anywhere a value is computed from the running scope, it is a FEEL expression.
Examples​
Simple Calculation​
(Monthly Income * 12) + Bonus
Conditional Logic​
if Age < 18 then "Minor" else "Adult"
Working with Dates​
date("2023-12-25") + duration("P1D") // Returns 2023-12-26
List Operations​
sum([10, 20, 30]) // Returns 60
List Filtering​
orderItems[item.price > 100] // Items costing more than 100
Next Steps​
Explore the detailed specifics of the language in the Syntax Guide, the standard library in Built-in Functions, and the editor integration in Tooling Support. For a one-page reference with runnable examples, see the FEEL cheat sheet.
FAQ​
What is FEEL?
FEEL stands for Friendly Enough Expression Language. It is the standardised expression language defined by the OMG DMN specification, designed to be readable by business users while being precise enough for automated execution. FEEL is purely functional and side-effect free - every expression returns a value, and evaluating the same expression twice with the same inputs always produces the same result. It has first-class support for numbers, strings, booleans, dates, times, durations, lists, and contexts (key-value objects), plus three-valued logic (true / false / null) for handling missing data cleanly.
What's the difference between FEEL and DMN?
DMN (Decision Model and Notation) is the modelling notation - boxes, decision tables, dependency graphs, hit policies. FEEL is the expression language used inside the DMN model: every cell of a decision table, every literal expression, every context entry is written in FEEL. People often conflate them because DMN introduced FEEL, but they're separate concerns: DMN tells you the shape of your decision logic, FEEL tells you how to write the individual expressions inside that logic. QuantumBPM also uses FEEL outside DMN - sequence-flow conditions, I/O mappings, timer expressions, and many more places in BPMN.
Where can I use FEEL in BPMN?
FEEL is the expression language for every dynamic value in a BPMN model. The most common places: sequence-flow conditionExpression (gateway routing), <quantum:ioMapping> source expressions on every task, script-task bodies, timer expressions (timeDuration, timeDate, timeCycle), <completionCondition> on multi-instance and ad-hoc sub-processes, <loopCardinality> and <inputCollection> for multi-instance, <loopCondition> for standard loops, activeElementsCollection on ad-hoc sub-processes, <quantum:subscription correlationKey> for message correlation, <conditionalEventDefinition> conditions, and user-task assignment fields. In short: anywhere a value is computed from the running scope, it is FEEL.
What's the difference between FEEL and JavaScript or JUEL?
FEEL is side-effect free by design - expressions return a value and cannot mutate state, perform I/O, or call out to the network. JavaScript and JUEL are imperative and can do all of those things. That makes FEEL safe to evaluate inside a deterministic workflow engine (re-evaluating a FEEL expression during replay always returns the same value), and impossible to use for things like "fetch this URL". FEEL also has different syntax - equality is = (not ==), if-then-else is an expression (not a statement), there are no semicolons, and built-in function names can contain spaces (string length, date and time). Use FEEL for routing, conditions, and value reshaping, use a service task with a worker for anything that touches the outside world.
Does QuantumBPM use full FEEL or S-FEEL?
Full FEEL. S-FEEL (Simple FEEL) is the unary-test subset of the language defined by the DMN spec for use in decision-table input entries - comparisons, intervals, lists, and not(). QuantumBPM implements both: full FEEL for every expression, and the unary-test subset for decision-table inputs, which is just FEEL parsed in unary-test mode. You can use the entire FEEL surface - for loops, if-then-else, list filters, function definitions, contexts, temporal types - anywhere the engine evaluates a FEEL expression.
When does my FEEL expression need a leading `=`?
In BPMN, always. A value that starts with = is a FEEL expression and anything else is a literal, on every field. So type="=workerType" resolves the worker type from a variable, while type="workerType" is the literal string. On fields that only ever hold an expression, such as <quantum:ioMapping> sources, sequence-flow conditions, multi-instance cardinality and collections, and correlation keys, a value without the = still works, but write it anyway. Two places take no =: a script task's script body, which scriptFormat already marks as FEEL, and DMN, where literal expressions and decision-table cells are always FEEL. Inside an expression an unquoted word is a variable reference, so a literal string keeps its FEEL quotes, ="active" rather than =active.
What's the difference between `=` and `==` in FEEL?
FEEL uses = for equality and there is no ==. So customer.tier = "vip" is the FEEL way to check equality - coming from JavaScript or Java it looks like assignment, but FEEL has no assignment because it has no side effects. Inequality is != (same as most languages). Other comparisons (<, <=, >, >=) work as expected. The single-equals convention surprises people often enough that it's worth memorising up front, especially when porting expressions from other rule engines.
How do I write an if-then-else in FEEL?
if condition then value-if-true else value-if-false. It is an expression, not a statement, so it always produces a value and can be nested or composed: if age < 18 then "minor" else if age < 65 then "adult" else "senior". The else branch is required - there is no "if without else" in FEEL - because every expression must produce a value. If you want a value to be missing in one branch, use null explicitly: if shouldCharge then amount else null.
How do I filter a list in FEEL?
list[predicate] - square brackets after a list filter it by a predicate, with item as the implicit element variable. For example, orderItems[item.price > 100] returns the items whose price is over 100. You can also filter by index: orderItems[1] is the first item (FEEL is 1-indexed). Combined with other built-ins this gives compact pipelines: count(orderItems[item.shipped = true]) counts the shipped items. The same filter syntax works inside for loops, function arguments, and anywhere a list is expected.
Is FEEL type-strict?
Yes. FEEL does not perform implicit type conversions - the number 123 is not equal to the string "123", and comparisons across incompatible types either return null or raise an error depending on the operation. The same applies to message correlation: a subscriber whose correlation key resolves to 123 (number) will not match a publisher sending "123" (string). Use the built-ins number("123") and string(123) to convert explicitly when needed. The strict typing is intentional - implicit coercion is the source of many subtle decision-table bugs in other rule engines.
How does FEEL handle null and missing variables?
FEEL uses three-valued logic: every operation can return true, false, or null. A path into a context that has no such key returns null (not an error), at any depth. Logic and comparisons carry null through: null = 5 is false, null > 5 is null, null and true is null (unknown) and null or true is true. Arithmetic and most built-in functions do not: the DMN spec defines null + 5 or string length(null) as an execution error, and QuantumBPM reports it as one (cannot perform arithmetic operation Plus with null operand) instead of letting a silent null flow into the next decision. So guard optional fields before you compute with them. To check for a missing value, you have two options: compare directly (value = null is true when the value is missing, value != null is true when it's present), or use the is() built-in (is(value, null) returns true when value is null). Both work, is() performs a strict identity check (same type and value), while = follows FEEL equality semantics. The one case that is an error is a bare name that is not in scope at all: discountCode != null raises unknown identifier discountCode when no variable of that name exists. Check it through its parent instead, for example order.discountCode != null.
Does FEEL have an `is defined` function?
No. is defined(x) is a Camunda extension, not part of the DMN standard, and QuantumBPM does not implement it, so it fails with unknown identifier is defined. Use a null check on the path instead: order.customer.email != null is false when email, customer or both are missing, and true when the field is there, because a path to a missing key returns null at any depth. For a key whose name is computed at runtime, get value(order, "discount") != null does the same. Try the null check in the playground.
Why does `substring` return the wrong characters in FEEL?
FEEL strings are 1-based: the first character is at position 1, not 0. substring("QB-2026-001", 4, 4) returns "2026", while a start position of 0 returns an empty string without an error, which is the usual symptom. A negative start counts from the end, so substring("QB-2026-001", -3) returns "001". There is no substr, use substring or the substring before and substring after functions from Built-in Functions. Try it in the playground.
How do I check whether any or all items in a list match a condition in FEEL?
With some and every, written without brackets: some item in orderItems satisfies item.price > 100 is true if at least one item costs more than 100, and every item in orderItems satisfies item.price > 100 is true only if all of them do. Writing it like a function call, some(item in orderItems satisfies ...), is a parse error (expected identifier, got (). The variable name is yours to choose, and you can bind several lists at once: some a in listA, b in listB satisfies a = b. Try it in the playground.
How do I get the month or year of a date in FEEL?
Date components are properties, not functions: date("2026-09-23").month is 9, .year is 2026, .day is 23 and .weekday is 3 (Monday is 1). The same works on today() and on date-times, so today().month is the current month number. month(today()) and year(today()) do not exist and fail with unknown identifier. month of year(...) does exist, but it returns the month name ("September"), not the number. Try it in the playground.
How do I find one record in a list of objects by a key in FEEL?
Filter the list on the key, then take the first match: departments[code = "DDE"][1].approved. Inside the filter, the fields of each element are in scope by name (or as item.code), and the filter always returns a list, so [1] unwraps the single match. Putting the key value in the brackets instead, departments.approved["DDE"], is not a lookup: it filters the list of approved values by the constant string and quietly returns []. Try the lookup in the playground.
How do I map over a list in FEEL?
There is no map function or arrow syntax. Iteration is a for expression that returns a new list: for item in orderItems return item.price * 2. To pick one field from every element you don't even need a loop, a path on a list projects it: orderItems.price returns the list of prices. To keep only some elements, use a filter, orderItems[price > 100]. Try it in the playground.
How do I build a context from a list of keys in FEEL?
Generate {key: ..., value: ...} entries with a for and pass them to context(): context(for code in codes return {key: code, value: code in approved}) turns ["LIT", "CMP", "DDE"] into {LIT: true, CMP: false, DDE: true}. Inside the loop use the loop variable (code), not a fixed variable from outside it, or every entry gets the same value. The reverse direction is get entries(ctx), which returns the key and value pairs as a list. Try it in the playground.
How do I parse a number with a comma as the decimal separator in FEEL?
Pass the grouping and decimal separators to number(): number("1.234,56", ".", ",") and number("1 234,56", " ", ",") both return 1234.56. The grouping separator can be a space, a comma or a period, and the decimal separator a comma or a period. With one argument number() only accepts the plain 1234.56 form, and number("1.234,56") fails with invalid numeric string. Try it in the playground.
What FEEL tooling does the QuantumBPM modeler provide?
The modeler embeds a full FEEL Language Server Protocol (LSP) implementation, surfacing language services live as you type. You get real-time syntax validation with line-and-column-accurate diagnostics, autocomplete for keywords, all ~80 standard built-in functions, and the variables actually in scope at the cursor position - including nested context paths and properties inside list filters (so typing customer. proposes the fields under customer, and typing items[item. proposes the per-item properties). Hover documentation provides signatures, parameter details, return types, and usage examples. Signature help shows the current argument while you type inside a function call, and semantic-token syntax highlighting marks keywords, built-ins, variables, and literals distinctly. The same LSP server runs the FEEL Playground for try-it-in-the-browser editing and is also available as a standalone binary for use in VS Code, Neovim, Emacs, or any LSP-compatible editor - see Tooling Support.
Is there a FEEL playground to try expressions without installing anything?
Yes - feel-playground.quantumbpm.com is an in-browser FEEL editor that runs the same LSP server the modeler uses. You get autocomplete, hover documentation, signature help, semantic syntax highlighting, and live diagnostics as you type, plus immediate evaluation of the result. Every expression has a shareable URL: paste the link into a chat or a ticket and the recipient sees the exact expression and inputs you were working with - useful for sharing snippets in code review, reproducing a tricky case for support, or teaching a teammate a particular FEEL pattern.