Skip to main content

FEEL Reference

FEEL (Friendly Enough Expression Language) is the expression language used throughout Muonroi for rule conditions, decision table logic, and workflow calculations. This guide covers syntax, functions, and best practices.

API Overview

The FEEL evaluation service is exposed at:

  • POST /api/v1/feel/evaluate — Evaluate a FEEL expression
  • POST /api/v1/feel/autocomplete — Get expression suggestions
  • GET /api/v1/feel/examples — Fetch example expressions

Evaluate Endpoint

Request payload:

{
"expression": "if score >= 80 then \"pass\" else \"fail\"",
"context": {
"score": 92
}
}

Response:

{
"result": "pass",
"type": "String"
}

Expression Types

1. Full FEEL Expressions

Complete DMN FEEL specification with support for complex logic, functions, and control flow.

Use cases:

  • Complex conditional logic
  • Multi-step calculations
  • List/array operations
  • Custom business logic

Syntax:

if score >= 80 then "pass" else "fail"

Performance: Compiled on first use, cached thereafter.

2. Unary Tests (Input Column Expressions)

Simplified syntax for decision table input columns. A unary test evaluates whether an input value matches a condition.

Format: expression where input is the implicit variable.

Use cases:

  • Decision table row matching
  • Input column validation
  • Range/list membership checks

Unary Test Syntax

Unary tests are used in decision table input columns to match incoming values.

Comparison Operators

Compare input to a literal value:

> 100           // Greater than
>= 50 // Greater than or equal
< 10 // Less than
<= 5 // Less than or equal
= 42 // Equal
!= 0 // Not equal

Examples in a decision table:

ScoreResult
>= 80Pass
>= 60Fail
elseRetry

Evaluates to:

input >= 80  // If input=92 → true
input >= 60 // If input=92 → false (already matched above)

Range Syntax

Match values within a range using bracket notation:

[1..100]        // Inclusive: 1 ≤ input ≤ 100
[1..100) // Inclusive-exclusive: 1 ≤ input < 100
(1..100] // Exclusive-inclusive: 1 < input ≤ 100
(1..100) // Exclusive: 1 < input < 100

Examples:

[0..18)         // Ages 0-17 (minors)
[18..65] // Ages 18-65 (working age)
(65..120] // Ages 65+ (seniors)

Decision table usage:

AgeCategory
[0..18)Minor
[18..65]Adult
(65..120]Senior

List/Enumeration Syntax

Match one of several values:

"Gold","Silver","Platinum"    // String list
1,2,3,5,8 // Numeric list
true,false // Boolean list

Examples:

"NY","CA","TX"                 // US states
"Premium","Standard" // Subscription tiers

Decision table usage:

TierPrice
Premium,Gold99.99
Standard49.99
Free,Trial0

Null/Empty Checks

null            // Matches null/undefined
not(null) // Matches non-null values
- // Wildcard: matches any value (including null)

Decision table usage:

StatusAction
nullInitialize
not(null)Update

Negation

Negate a value or list:

not("Bronze")                  // NOT equal to "Bronze"
not(1,2,3) // NOT in list [1,2,3]

Examples:

not("Inactive")                // Exclude inactive users
not([0..18)) // NOT minors (i.e., >= 18)

Expression vs Unary Test

Key Differences

FeatureUnary TestFull Expression
ContextInput column onlyAny context variables
SyntaxShorthand (> 100)Full FEEL syntax
VariablesImplicit inputExplicit variable names
Use caseDecision table matchingRules, output fields, workflows

Examples

Unary Test (decision table input):

>= 80      // input >= 80

Full Expression (rule condition):

score >= 80 and attempts <= 3

Output field expression:

if score >= 80 then "pass" else if score >= 60 then "fail" else "retry"

Control Flow

If-Then-Else

if condition then value1 else value2

Example:

if score >= 80 then "pass" else "fail"

if age >= 18 and country = "US"
then "eligible"
else "ineligible"

For Expression

Iterate over a list:

for x in list return x + 1

Example:

for item in items return item.price * 1.1  // Add 10% to each price

Quantified Expressions

Check list membership:

some x in list satisfies x > 100      // At least one > 100
every x in list satisfies x > 0 // All are positive

Example:

some item in cartItems satisfies item.quantity > 10  // Bulk order

Standard Library Functions

String Functions

FunctionSignatureDescriptionExample
stringstring(val)Convert to stringstring(123)"123"
string lengthstring_length(str)Character countstring_length("hello")5
substringsubstring(str, start, [length])Extract substringsubstring("hello", 1, 2)"el"
upper caseupper_case(str)Convert to uppercaseupper_case("hello")"HELLO"
lower caselower_case(str)Convert to lowercaselower_case("HELLO")"hello"
substring beforesubstring_before(str, match)Text before matchsubstring_before("hello world", " ")"hello"
substring aftersubstring_after(str, match)Text after matchsubstring_after("hello world", " ")"world"
replacereplace(str, search, replace, [flags])Replace textreplace("hello", "l", "L")"heLLo"
containscontains(str, search)Check if containscontains("hello", "ll")true
starts withstarts_with(str, prefix)Check prefixstarts_with("hello", "he")true
ends withends_with(str, suffix)Check suffixends_with("hello", "lo")true
matchesmatches(str, regex)Regex matchmatches("test123", "[0-9]+")true
splitsplit(str, delimiter)Split into listsplit("a,b,c", ",")["a", "b", "c"]

Numeric Functions

FunctionSignatureDescriptionExample
decimaldecimal(str)Parse decimaldecimal("3.14")3.14
floorfloor(num)Round downfloor(3.7)3
ceilingceiling(num)Round upceiling(3.2)4
roundround(num, [digits])Round to precisionround(3.14159, 2)3.14
absabs(num)Absolute valueabs(-5)5
modulomodulo(a, b)Remaindermodulo(10, 3)1
sqrtsqrt(num)Square rootsqrt(16)4
loglog(num)Natural logarithmlog(2.718)1
expexp(num)e raised to powerexp(1)2.718
oddodd(num)Check if oddodd(3)true
eveneven(num)Check if eveneven(4)true

List Functions

FunctionSignatureDescriptionExample
list containslist_contains(list, value)Check membershiplist_contains([1,2,3], 2)true
countcount(list)Count elementscount([1,2,3])3
minmin(list)Minimum valuemin([3,1,2])1
maxmax(list)Maximum valuemax([3,1,2])3
sumsum(list)Sum all elementssum([1,2,3])6
meanmean(list)Average valuemean([1,2,3])2
allall(list)All true (AND)all([true, true])true
anyany(list)Any true (OR)any([true, false])true
appendappend(list, value)Add to endappend([1,2], 3)[1,2,3]
concatenateconcatenate(list1, list2, ...)Merge listsconcatenate([1,2], [3,4])[1,2,3,4]
insert beforeinsert_before(list, position, value)Insert at indexinsert_before([1,3], 2, 2)[1,2,3]
removeremove(list, value)Remove valueremove([1,2,3], 2)[1,3]
reversereverse(list)Reverse orderreverse([1,2,3])[3,2,1]
index ofindex_of(list, value)Find positionindex_of([1,2,3], 2)2
unionunion(list1, list2)Combine uniqueunion([1,2], [2,3])[1,2,3]
distinct valuesdistinct_values(list)Remove duplicatesdistinct_values([1,1,2])[1,2]
flattenflatten(list)Flatten nested listflatten([[1,2], [3,4]])[1,2,3,4]
sortsort(list)Sort ascendingsort([3,1,2])[1,2,3]

Date/Time Functions

FunctionSignatureDescriptionExample
todaytoday()Current datetoday()date(2026,3,20)
nownow()Current date-timenow()date and time(2026,3,20, 10:30:00)
datedate(year, month, day)Create datedate(2026, 3, 20)
timetime(hour, minute, [second])Create timetime(14, 30, 0)
date and timedate_and_time(date, time)Combine date+timedate_and_time(date(...), time(...))
durationduration(str)Parse durationduration("P1Y2M3D") → ISO 8601 duration
years and months durationyears_and_months_duration(from, to)Months between datesyears_and_months_duration(date1, date2)
days and time durationdays_and_time_duration(from, to)Days + hours betweendays_and_time_duration(date1, date2)
day of weekday_of_week(date)Day number (1-7)day_of_week(date(2026, 3, 20))5 (Friday)
day of yearday_of_year(date)Day number (1-365)day_of_year(date(2026, 1, 1))1
week of yearweek_of_year(date)Week number (1-53)week_of_year(date(2026, 3, 20))12
month of yearmonth_of_year(date)Month number (1-12)month_of_year(date(2026, 3, 20))3
yearyear(date)Extract yearyear(date(2026, 3, 20))2026

Context Functions

FunctionSignatureDescriptionExample
get entriesget_entries(context)List all key-value pairsget_entries(myContext)
get valueget_value(context, key)Get by keyget_value(myContext, "name")

Keywords

Reserved words in FEEL expressions:

  • if, then, else — Conditional logic
  • for, in — Iteration
  • some, every, satisfies — List quantification
  • between, and — Range checks
  • instance of — Type checks
  • or, not — Boolean operators

IFeelCellEvaluator Interface

The IFeelCellEvaluator interface evaluates FEEL expressions within decision tables.

Interface Definition

public interface IFeelCellEvaluator
{
/// <summary>
/// Evaluates a single input-cell expression against an input value.
/// </summary>
bool Evaluate(string expression, object? inputValue, string? columnDataType = null);

/// <summary>
/// Validates expression syntax. Returns null when valid; otherwise an error message.
/// </summary>
string? Validate(string expression, string? columnDataType = null);
}

Implementations

FullFeelCellEvaluator (default)

  • Supports complete FEEL syntax for unary tests
  • Normalizes shorthand syntax (> 100input > 100)
  • Type coercion for numeric, boolean, date types
  • Falls back to simplified evaluator on parse errors

SimplifiedFeelCellEvaluator

  • Fast evaluation subset of FEEL
  • Optimized for common decision table patterns
  • No external dependency compilation
  • Used as fallback in FullFeelCellEvaluator

Example: Custom Evaluator

public class CustomFeelEvaluator : IFeelCellEvaluator
{
public bool Evaluate(string expression, object? inputValue, string? columnDataType = null)
{
// Custom logic: e.g., domain-specific operators
if (expression == "premium_range")
{
return inputValue is >= 100 and < 1000;
}

// Delegate to built-in evaluator
return new FullFeelCellEvaluator().Evaluate(expression, inputValue, columnDataType);
}

public string? Validate(string expression, string? columnDataType = null)
{
return null; // Custom validation
}
}

Registration

services.AddDecisionTableEngine()
.ConfigureFeel(options =>
{
options.CellEvaluator = new CustomFeelEvaluator();
});

Decision Table Integration Examples

Input Column with Unary Tests

Decision table:

AgeTierDiscount
[0..18)Child50%
[18..65]Adult0%
(65..120]Senior20%

Evaluation:

var table = await engine.EvaluateTableAsync("PricingTable", new { age = 35 });
// Input "35" matches row 2: [18..65] → Adult, 0%

Output Column with Expressions

Decision table:

ScoreRating
>= 90A
>= 80B
>= 70C
elseF

Output column can use expressions:

if score >= 90 then "Excellent" else if score >= 80 then "Good" else "Needs Improvement"

Cross-Column References

Decision table with computed output:

UnitsPriceTotal
> 10010units * 9
else10units * 10

Expression: units * price * 0.9 (for units > 100)


Best Practices

1. Keep Expressions Readable

Bad:

if x>10&&y<20||z==30then"ok"else"fail"

Good:

if (x > 10 and y < 20) or z = 30 then "ok" else "fail"

2. Use Type Hints in Decision Tables

var options = new DecisionTableOptions
{
ColumnDataTypes = new Dictionary<string, string>
{
["age"] = "numeric",
["joinDate"] = "date",
["status"] = "string"
}
};

3. Null Safety

Always check for null in complex expressions:

if name != null and string_length(name) > 0 then name else "Unknown"

4. List Operations

Use list functions for cleaner logic:

if list_contains(["NY", "CA", "TX"], state) then "major_hub" else "standard"

5. Performance

  • Cache compiled expressions at runtime
  • Use simplified FEEL for high-throughput operations
  • Avoid deeply nested conditionals

Error Handling

Validation Errors

var validator = evaluator as IFeelCellEvaluator;
string? error = validator?.Validate("> 100", "numeric");
if (error != null)
{
Console.WriteLine($"Validation error: {error}");
}

Type Coercion

FEEL automatically coerces types when possible:

"42" > 40      // String "42" coerced to 42, true

If coercion fails, the expression returns null:

"abc" > 40     // Cannot coerce "abc" to number → null → false

See Also