Comprehensive Reference for the Firefly Framework Rule Engine YAML Domain Specific Language
Based on actual AST-based parser implementation analysis
New to the Rule Engine? Start with our layered documentation approach:
- 🚀 Quick Start Guide - Get started in 15 minutes with essential syntax
- 🎯 Common Patterns Guide - Real-world examples organized by complexity
- 🏛️ Governance Guidelines - Team standards and feature selection guidance
- 📖 This Reference - Complete syntax documentation (you are here)
- Introduction
- DSL Structure Overview
- Reserved Keywords
- Variable Types and Naming
- Core DSL Elements
- Action Syntax
- Condition Syntax
- Expression Types
- Built-in Functions
- Advanced Features
- Complete Examples
The Firefly Framework Rule Engine uses a powerful YAML-based Domain Specific Language (DSL) that is parsed using an Abstract Syntax Tree (AST) approach. This guide documents the actual implementation based on comprehensive codebase analysis, ensuring accuracy and completeness.
💡 First Time Here? This is a comprehensive reference. For a gentler introduction, start with the Quick Start Guide or browse Common Patterns for real-world examples.
- AST-Based Parsing: All syntax is parsed into strongly-typed AST nodes
- Expression-Driven: Actions and conditions support complex expressions
- Type-Safe: Variables have strict naming conventions for automatic type resolution
- Extensible: Support for REST API calls, JSON manipulation, and custom functions
- 🔍 Find Specific Syntax: Use the table of contents or search for keywords
- 📋 Copy Examples: All complete-rule code examples in this file are parsed by the
DocExamplesValidationTestat every build -- if you see one fail in your fork, the doc is out of sync with the implementation. - 🎯 Choose Complexity: See Governance Guidelines for feature selection advice
- 🚀 Get Started: Try examples from Quick Start Guide first
What it is. A stateless expression-evaluation engine over a single input map.
You hand it a parsed YAML rule and a Map<String, Object> of inputs; it returns a
result with computed outputs, a condition outcome, and timing/audit metadata.
What it is NOT. This is not Drools-style rule-based reasoning. There is no working memory, no fact base, no inference, no rule-chaining triggered by data changes. Each evaluation is an independent function call.
| Capability | Supported by Firefly Rule Engine |
|---|---|
| Rule definitions in YAML | ✅ |
| 30+ comparison and validation operators | ✅ |
Constants from DB (auto-detected by UPPER_CASE) |
✅ |
forEach / while / do-while loops |
✅ |
Sub-rules (rules: block) with shared state across rules in one eval |
✅ |
Sub-rule priority (drools-style salience via priority: N) |
✅ |
Inline conditional expression (if_else(cond, then, else)) |
✅ |
Decision tables (DMN-style) -- decision_table: block with FIRST / COLLECT / ANY / UNIQUE hit policies |
✅ |
Rule composition -- invoke_rule(code, ...) evaluates a stored rule and returns its outputs |
✅ |
Per-rule timeout -- timeout: 5s declarative budget enforced via Reactor Mono.timeout() |
✅ |
Input defaults -- declare default: per input; caller-omitted values are filled in |
✅ |
Custom function registry (Spring @Component) |
✅ |
| REST / JSON path built-ins | ✅ |
| Circuit breaker action (early termination) | ✅ |
| Rule chaining across separate evaluations -- output of one eval automatically firing another | ❌ |
Persistent working memory / fact base like Drools KIE |
❌ |
| Inference / forward-chaining -- deriving new facts that fire more rules | ❌ |
| Backward chaining (goal-driven reasoning) | ❌ |
| Cross-input joins -- finding pairs/groups of inputs that satisfy a constraint | ❌ |
Short-circuit evaluation in function calls -- if_else(cond, X, Y) evaluates both branches |
❌ |
| Truth maintenance / retraction | ❌ -- variables are write-once-per-eval and never retracted |
If you need any of the "❌" capabilities, this engine is the wrong tool. For those use cases consider Drools / OpenL Tablets / DMN engines. For everything else -- deterministic rule evaluation over an input payload -- this engine is purpose-built to be smaller, faster to onramp, and clearer to reason about.
name: "Rule Name" # Required: Human-readable identifier
description: "Rule description" # Required: Purpose and behavior
inputs: [variable1, variable2] # Required: Runtime input variables
output: {result: type} # Required: Output variable mappingversion: "1.0.0" # Optional: Version tracking
metadata: # Optional: Additional metadata
tags: ["tag1", "tag2"]
author: "Team Name"
category: "Category"
constants: # Optional: Constants with defaults
- code: CONSTANT_NAME
defaultValue: value
timeout: 5s # Optional: per-rule wall-clock budget
# Accepts "5s", "500ms", or raw milliseconds.
# Exceeding it fails the rule with a clean message.inputs: accepts three shapes:
# 1. Flat list -- type defaults to Object
inputs: [creditScore, annualIncome, age]
# 2. Name -> type
inputs:
creditScore: number
annualIncome: number
age: number
# 3. Name -> {type, default} -- default is injected when caller omits the variable
inputs:
creditScore:
type: number
default: 0
annualIncome:
type: number
default: 0
region:
type: string
default: "UNKNOWN"Caller-supplied values always override declared defaults. This makes rules safer to evaluate from partial inputs without sprinkling coalesce(...) calls through every action.
Note: early versions accepted a top-level
circuit_breaker:configuration block (enabled,failure_threshold,timeout_duration,recovery_timeout). It was parsed but never enforced at runtime and is no longer accepted. Use thecircuit_breaker "MESSAGE"action (described under "Action Syntax") for controlled early termination within a rule.
Simple Syntax:
when: [conditions] # Simple condition list
then: [actions] # Actions when true
else: [actions] # Actions when false (optional)Complex Syntax:
conditions: # Structured condition blocks
if: {condition_structure}
then: {action_structure}
else: {action_structure}Multiple Rules:
rules: # Array of sub-rules
- name: "Sub-rule 1"
priority: 10 # Optional: higher salience runs first (default 0)
when: [conditions]
then: [actions]
- name: "Sub-rule 2"
priority: 1 # Lower priority -- evaluates after Sub-rule 1
when: [conditions]
then: [actions]Sub-rule priority is drools-style salience: higher priority: evaluates first; ties preserve YAML declaration order via a stable sort. Omitted priorities default to 0.
Decision Table (DMN-style):
decision_table:
inputs: [col1, col2] # Optional: input column names (for documentation)
outputs: [col_a, col_b] # Optional: output column names
hit_policy: FIRST # FIRST | COLLECT | ANY | UNIQUE (default FIRST)
rules:
- when: [predicate, predicate] # Each row is a list of `when:` predicates
then: { col_a: value, col_b: value }
- otherwise: true # Fallback row -- matches when no others did
then: { col_a: default_value, col_b: default_value }See the Decision Tables section below for the full syntax, hit-policy semantics, and the = prefix for expression outputs.
The DSL uses specific reserved keywords that have special meaning in the parser. These are organized by category for easy reference:
Several keywords have multiple accepted spellings -- a deliberate flexibility so the DSL reads naturally in different contexts. The canonical form is the one we recommend in new code; aliases remain accepted for compatibility. All synonyms below are matched case-insensitively where indicated.
| Canonical | Aliases (also accepted) | Notes |
|---|---|---|
equals |
== |
Prefer equals in prose-style conditions, == in expressions |
not_equals |
!= |
Same convention |
greater_than |
> |
Use the symbol in expressions, the keyword in conditions |
less_than |
< |
Same |
at_least |
greater_than_or_equal, >= |
at_least reads most naturally in financial rules |
at_most |
less_than_or_equal, <= |
Same |
in_list |
in |
in_list makes membership intent explicit |
not_in_list |
not_in |
Same |
is_not_null |
(no alias) | Use this rather than not is_null -- the negated form is one operator |
not_contains |
(no alias) | Same -- one operator, not not contains |
| Canonical | Aliases | Notes |
|---|---|---|
and |
AND, && |
Lower-case in YAML by convention; upper-case is also matched |
or |
OR, || |
Same |
not |
NOT, ! |
Unary; prefer not x is_email over not_email |
| Canonical | Aliases | Notes |
|---|---|---|
forEach |
for |
Both reach the same parser path; forEach reads better in mixed-case YAML |
These are exact synonyms within the function-call layer -- pick one and stick with it inside a rule for readability:
| Canonical | Aliases | What it does |
|---|---|---|
length |
len |
String / list length |
count |
size |
Collection size |
avg |
average |
Mean of a list |
uppercase |
upper |
String → uppercase |
lowercase |
lower |
String → lowercase |
substring |
substr |
Extract substring |
tonumber |
number |
Coerce to number |
tostring |
string |
Coerce to string |
toboolean |
boolean |
Coerce to boolean |
json_get |
json_path |
Extract value from JSON via path |
is_in_range |
in_range |
Inclusive bounded check |
if_else |
ifelse |
Inline conditional value |
| Canonical | Also accepted | Notes |
|---|---|---|
inputs |
input |
The parser merges both into the same model field; prefer inputs |
outputs |
output |
Same |
Removed in 26.05.08: the top-level
circuit_breaker:configuration block (withenabled,failure_threshold,timeout_duration,recovery_timeoutsub-keys) was parsed but never enforced at runtime. The action-formcircuit_breaker "MESSAGE"(described in Action Syntax) is the only circuit-breaker surface and is unchanged.
🏗️ Structural Keywords - Define the rule structure and metadata
| Section | Keywords | Required | Purpose | Example |
|---|---|---|---|---|
| Rule Metadata | name |
✅ | Human-readable rule identifier | name: "Credit Assessment" |
description |
✅ | Rule purpose and behavior | description: "Evaluates credit applications" |
|
version |
❌ | Version tracking | version: "1.0.0" |
|
| Data Definitions | inputs |
✅ | Runtime input variables | inputs: [creditScore, annualIncome] |
output |
✅ | Output variable mapping | output: {approval_status: text} |
|
constants |
❌ | System constants with defaults | constants: [{code: MIN_SCORE, defaultValue: 650}] |
|
| Logic Structure | when |
❌* | Simple condition syntax | when: [creditScore >= 650] |
then |
❌* | Actions when conditions true | then: [set status to "APPROVED"] |
|
else |
❌ | Actions when conditions false | else: [set status to "DECLINED"] |
|
conditions |
❌* | Complex condition blocks | conditions: {if: {...}, then: {...}} |
|
rules |
❌* | Multiple sequential rules | rules: [{name: "Rule 1", when: [...]}] |
|
| Advanced Features | metadata |
❌ | Additional metadata | metadata: {tags: ["credit"], author: "Team"} |
*One of when/then, conditions, or rules is required for logic definition.
🎯 Action Keywords - Define what the rule should do
| Keyword | Purpose | Syntax | Example |
|---|---|---|---|
set |
Variable assignment | set variable to value |
set approval_status to "APPROVED" |
calculate |
Mathematical expression | calculate variable as expression |
calculate debt_ratio as monthlyDebt / annualIncome |
run |
Function/API invocation | run variable as function(args) |
run maximum as max(value1, value2) |
call |
Function invocation | call function with [args] |
call log with ["Message", "INFO"] |
forEach |
Loop over list | forEach item in list: action |
forEach num in numbers: calculate total as total + num |
for |
Loop over list (alias) | for item in list: action |
for item in items: set count to count + 1 |
while |
Conditional loop | while condition: action |
while counter less_than 10: add 1 to counter |
do |
Do-while loop | do: action while condition |
do: add 1 to counter while counter less_than 10 |
in |
Loop list specifier | Used with forEach/for | forEach item in items: ... |
if/then/else |
Conditional actions | if condition then action |
if creditScore > 700 then set tier to "PRIME" |
add |
Addition operation | add value to variable |
add 10 to base_score |
subtract |
Subtraction operation | subtract value from variable |
subtract penalty from total_score |
multiply |
Multiplication operation | multiply variable by value |
multiply risk_factor by 1.5 |
divide |
Division operation | divide variable by value |
divide monthly_payment by 2 |
append |
Add to list end | append value to list |
append "HIGH_RISK" to risk_factors |
prepend |
Add to list start | prepend value to list |
prepend "PRIORITY" to processing_flags |
remove |
Remove from list | remove value from list |
remove "TEMPORARY" from account_flags |
circuit_breaker |
Stop execution | circuit_breaker "message" |
circuit_breaker "HIGH_RISK_DETECTED" |
🔍 Logical Keywords - Define conditional logic and boolean operations
| Category | Keywords | Purpose | Syntax | Example |
|---|---|---|---|---|
| Logical Operators | and, AND |
Logical AND | condition1 AND condition2 |
creditScore >= 650 AND annualIncome > 50000 |
or, OR |
Logical OR | condition1 OR condition2 |
creditScore >= 750 OR hasCollateral == true |
|
not, NOT |
Logical NOT | NOT condition |
NOT (accountStatus == "SUSPENDED") |
|
| Conditional Structure | if |
Condition definition | if: condition |
if: {and: [condition1, condition2]} |
compare |
Comparison block | compare: {left, operator, right} |
compare: {left: age, operator: ">=", right: 18} |
|
| Helper Keywords | left, right |
Comparison operands | In compare blocks | left: creditScore, right: 650 |
operator |
Comparison operator | In compare blocks | operator: "greater_than" |
|
actions |
Action list | In complex syntax | actions: [{set: {...}}] |
|
variable, value |
Set action params | In complex syntax | variable: "status", value: "APPROVED" |
⚖️ Comparison Operators - Compare values and expressions
| Category | Operator | Aliases | Purpose | Example |
|---|---|---|---|---|
| Equality | == |
equals |
Equality check | status equals "ACTIVE" |
!= |
not_equals |
Inequality check | type not_equals "SUSPENDED" |
|
| Numeric | > |
greater_than |
Greater than | creditScore greater_than 700 |
< |
less_than |
Less than | age less_than 65 |
|
>= |
at_least, greater_than_or_equal |
Greater or equal | income at_least 50000 |
|
<= |
at_most, less_than_or_equal |
Less or equal | debt_ratio at_most 0.4 |
|
| Range | between |
- | Range inclusion | age between 18 and 65 |
not_between |
- | Range exclusion | score not_between 0 and 100 |
|
| String | contains |
- | String contains | name contains "Smith" |
not_contains |
- | String not contains | email not_contains "temp" |
|
starts_with |
- | String prefix | phone starts_with "+1" |
|
ends_with |
- | String suffix | email ends_with ".com" |
|
matches |
- | Regex match | ssn matches "^\\d{3}-\\d{2}-\\d{4}$" |
|
not_matches |
- | Regex not match | phone not_matches "^\\+1" |
|
| Length | length_equals |
- | Length-of-string equality | code length_equals 4 |
length_greater_than |
- | Length-of-string > |
password length_greater_than 7 |
|
length_less_than |
- | Length-of-string < |
nickname length_less_than 20 |
|
| List | in_list |
in |
List membership | status in_list ["ACTIVE", "PENDING"] |
not_in_list |
not_in |
List non-membership | type not_in_list ["SUSPENDED", "CLOSED"] |
|
| Existence | exists |
- | Variable existence | exists guarantorInfo |
is_null |
- | Null check | is_null previousLoan |
|
is_not_null |
- | Not null check | is_not_null collateralValue |
✅ Validation Operators - Validate data types and formats
| Category | Operator | Purpose | Syntax | Example |
|---|---|---|---|---|
| Existence Checks | exists |
Check if variable exists | exists variable |
exists guarantorInfo |
is_null |
Check if value is null | variable is_null |
previousLoan is_null |
|
is_not_null |
Check if value is not null | variable is_not_null |
collateralValue is_not_null |
|
| Type Checking | is_number |
Check if value is number | variable is_number |
creditScore is_number |
is_string |
Check if value is string | variable is_string |
customerName is_string |
|
is_boolean |
Check if value is boolean | variable is_boolean |
hasCollateral is_boolean |
|
is_list |
Check if value is list/array | variable is_list |
riskFactors is_list |
|
| Basic Content | is_empty |
Check if value is empty | variable is_empty |
customerName is_empty |
is_not_empty |
Check if value is not empty | variable is_not_empty |
email is_not_empty |
|
is_numeric |
Check if string is numeric | variable is_numeric |
inputValue is_numeric |
|
is_not_numeric |
Check if string is not numeric | variable is_not_numeric |
customerName is_not_numeric |
|
| Format Validation | is_email |
Validate email format | variable is_email |
contactEmail is_email |
is_phone |
Validate phone number format | variable is_phone |
phoneNumber is_phone |
|
is_date |
Validate date format | variable is_date |
birthDate is_date |
|
| Numeric Properties | is_positive |
Check if number > 0 | variable is_positive |
loanAmount is_positive |
is_negative |
Check if number < 0 | variable is_negative |
accountBalance is_negative |
|
is_zero |
Check if number equals 0 | variable is_zero |
outstandingDebt is_zero |
|
| Financial Formats | is_percentage |
Validate percentage format | variable is_percentage |
interestRate is_percentage |
is_currency |
Validate currency format | variable is_currency |
monthlyIncome is_currency |
|
is_credit_score |
Validate credit score range (300-850) | variable is_credit_score |
creditScore is_credit_score |
|
is_ssn |
Validate SSN format (XXX-XX-XXXX) | variable is_ssn |
socialSecurityNumber is_ssn |
|
is_account_number |
Validate bank account number | variable is_account_number |
bankAccount is_account_number |
|
is_routing_number |
Validate bank routing number | variable is_routing_number |
routingNumber is_routing_number |
|
| Date Properties | is_business_day |
Check if date is business day | variable is_business_day |
applicationDate is_business_day |
is_weekend |
Check if date is weekend | variable is_weekend |
submissionDate is_weekend |
|
age_at_least |
Check minimum age requirement | variable age_at_least value |
customerAge age_at_least 18 |
|
age_less_than |
Check maximum age requirement | variable age_less_than value |
applicantAge age_less_than 65 |
Usage Examples:
when:
# Existence and null checks
- exists customerData
- is_not_null annualIncome
- previousLoan is_null
# Type validation
- creditScore is_number
- customerName is_string
- hasCollateral is_boolean
- riskFactors is_list
# Content validation
- email is_not_empty
- phoneNumber is_phone
- socialSecurityNumber is_ssn
# Numeric properties
- loanAmount is_positive
- interestRate is_percentage
- creditScore is_credit_score
# Date validation
- applicationDate is_business_day
- customerAge age_at_least 18
# Complex boolean expressions with validation operators (NEW FEATURE)
- (creditScore is_credit_score AND creditScore >= 650)
- (email is_email AND email is_not_empty)
- (monthlyIncome is_positive AND annualIncome is_positive)Validation Operators in Expressions
Validation operators can be used in any expression context, not just when: clauses:
then:
# Validation operators inside `set`-to-boolean expressions
- set has_valid_contact to (email is_email and phone is_phone)
- set financial_data_complete to (monthlyRevenue is_positive and monthlyExpenses is_positive and annualIncome is_not_null)
# Score each field independently with the inline `if_else` function, then sum
- run name_score as if_else(customerName is_not_empty, 25, 0)
- run email_score as if_else(email is_email, 25, 0)
- run phone_score as if_else(phone is_phone, 25, 0)
- run ssn_score as if_else(ssn is_ssn, 25, 0)
- calculate data_quality_score as name_score + email_score + phone_score + ssn_scoreNote: The engine does not have a C-style ternary
? :operator; use theif_else(condition, then_value, else_value)built-in function instead. Both arguments are evaluated eagerly (no short-circuit).
🔧 Arithmetic Operators & Keywords - Mathematical operations and helpers
| Category | Operator/Keyword | Symbol | Purpose | Syntax | Example |
|---|---|---|---|---|---|
| Basic Arithmetic | + |
+ |
Addition | expression + expression |
principal + interest |
- |
- |
Subtraction | expression - expression |
income - expenses |
|
* |
* |
Multiplication | expression * expression |
rate * amount |
|
/ |
/ |
Division | expression / expression |
monthlyDebt / annualIncome |
|
% |
% |
Modulo (remainder) | expression % expression |
amount % 100 |
|
** |
** |
Power/Exponentiation | base ** exponent |
(1 + rate) ** years |
|
| Arithmetic Actions | add |
- | Add value to variable | add VALUE to VARIABLE |
add 10 to base_score |
subtract |
- | Subtract value from variable | subtract VALUE from VARIABLE |
subtract penalty from total_score |
|
multiply |
- | Multiply target variable by factor | multiply VALUE by VARIABLE |
multiply 1.5 by risk_factor |
|
divide |
- | Divide target variable by divisor | divide VALUE by VARIABLE |
divide 2 by monthly_payment |
|
| Helper Keywords | to |
- | Assignment target | set VARIABLE to VALUE |
set approval_status to "APPROVED" |
as |
- | Calculation target | calculate VARIABLE as EXPRESSION |
calculate debt_ratio as monthlyDebt / annualIncome |
|
with |
- | Function parameters | call FUNCTION with [args] |
call log with ["Message", "INFO"] |
|
from |
- | Subtraction source | subtract VALUE from VARIABLE |
subtract penalty from total_score |
|
by |
- | Factor for multiply/divide | multiply VALUE by VARIABLE |
multiply 1.5 by risk_factor |
|
and |
- | Range separator | VALUE between MIN and MAX |
age between 18 and 65 |
Grammar peculiarity for
multiply/divide: the value comes first, then the variable. All four arithmetic actions follow the same shape:<keyword> <value-expression> <preposition> <target-variable>. Readmultiply 1.5 by risk_factoras "apply ×1.5 torisk_factor".
Arithmetic Expression Examples:
then:
# Basic arithmetic in expressions
- calculate monthly_income as annualIncome / 12
- calculate total_debt as creditCardDebt + loanDebt + mortgageDebt
- calculate compound_amount as principal * (1 + rate) ** years
- calculate remainder as loanAmount % 1000
# Arithmetic actions (modify existing variables)
# Grammar: <keyword> <value> <preposition> <target-variable>
- add 50 to credit_score
- subtract late_fee from account_balance
- multiply 1.2 by risk_score
- divide 2 by monthly_payment
# Complex expressions
- calculate debt_to_income as (monthlyDebt + proposedPayment) / (annualIncome / 12)
- calculate weighted_score as (creditScore * 0.6) + (incomeScore * 0.4)Operator Precedence (highest to lowest):
**(Power/Exponentiation)*,/,%(Multiplication, Division, Modulo)+,-(Addition, Subtraction)- Comparison operators (
>,<,>=,<=,==,!=) - Logical operators (
AND,OR,NOT)
Parentheses can be used to override precedence:
- calculate result as (a + b) * (c - d)
- calculate complex as ((x * y) + z) / (a - b)Source: API request inputData parameter
Naming: camelCase (e.g., creditScore, annualIncome)
Purpose: Dynamic runtime values
inputs:
- creditScore # From API inputData
- annualIncome # From API inputData
- employmentYears # From API inputDataSource: Database constants table
Naming: UPPER_CASE_WITH_UNDERSCORES (e.g., MIN_CREDIT_SCORE)
Purpose: Configuration values, business parameters
when:
- creditScore at_least MIN_CREDIT_SCORE # From database
- loanAmount less_than MAX_LOAN_AMOUNT # From databaseSource: Created during rule execution
Naming: snake_case (e.g., debt_ratio, final_score)
Purpose: Intermediate calculations, results
then:
- calculate debt_ratio as monthlyDebt / annualIncome # snake_case
- set approval_status to "APPROVED" # snake_casePurpose: Assign values to computed variables
Syntax: set variable_name to value
then:
- set approval_status to "APPROVED"
- set risk_score to 75
- set is_eligible to true
- set processing_date to "2025-01-15"Purpose: Evaluate expressions and store results
Syntax: calculate variable_name as expression
Key Insight: calculate is NOT limited to mathematical operations. It can evaluate:
- Mathematical expressions
- Function calls (including REST API calls)
- JSON path operations
- Complex nested expressions
then:
# Mathematical calculations
- calculate debt_ratio as monthlyDebt / (annualIncome / 12)
- calculate compound_interest as principal * (1 + rate) ** years
# Function calls
- run max_value as max(value1, value2, value3)
- calculate loan_payment as calculate_loan_payment(amount, rate, term)
# REST API calls
- run user_data as rest_get("https://api.example.com/users/123")
- run api_response as rest_post("https://api.example.com/data", requestBody)
# JSON operations
- run user_name as json_get(user_data, "name")
- run user_age as json_get(user_data, "age")
- run has_email as json_exists(user_data, "email")
# Complex expressions
- calculate risk_score as ((creditScore * 0.6) + (annualIncome / 1000 * 0.3) + 50)Purpose: Define conditions for rule execution
Syntax: List of condition strings
when:
- creditScore at_least 650
- annualIncome greater_than 40000
- employmentYears at_least 1
- customerType in_list ["PREMIUM", "GOLD"]Purpose: Define multiple sub-rules with sequential execution
rules:
- name: "Initial Assessment"
when: creditScore at_least 600
then:
- set initial_eligible to true
else:
- set initial_eligible to false
- name: "Final Decision"
when: initial_eligible equals true
then:
- set final_decision to "APPROVED"
else:
- set final_decision to "DECLINED"# Simple assignment
- set variable_name to value
# Assignment with expressions
- set monthly_income to annualIncome / 12
- set full_name to firstName + " " + lastName
# Complex boolean expressions with validation operators (NEW FEATURE)
- set has_complete_financial_data to (
monthlyRevenue is_positive AND
monthlyExpenses is_positive AND
existingDebt is_not_null AND
monthlyDebtPayments is_positive AND
verifiedAnnualRevenue is_positive
)
# Multi-line boolean expressions with parentheses
- set meets_basic_requirements to (
creditScore is_credit_score AND
creditScore >= MIN_CREDIT_SCORE AND
annualIncome is_positive AND
customerAge >= 18
)
# Validation operators in complex expressions
- set data_validation_passed to (
customerName is_not_empty AND
email is_email AND
phone is_phone AND
ssn is_ssn
)The rule engine provides two commands for computed values:
calculate - For mathematical operations only:
- Arithmetic expressions (
+,-,*,/,%,**) - Mathematical calculations with numbers
- Expressions that produce numeric results
run - For function invocations and external operations:
- Function calls (e.g.,
max(),min(),abs(),format_currency()) - REST API calls (e.g.,
rest_get(),rest_post()) - JSON operations (e.g.,
json_get(),json_exists()) - String functions (e.g.,
upper(),lower(),trim()) - Any operation that invokes a function or external service
# ✅ CORRECT: Use 'calculate' for mathematical operations
- calculate total as amount + tax
- calculate monthly_payment as principal * rate / (1 - (1 + rate) ** -term)
- calculate debt_ratio as monthlyDebt / annualIncome
- calculate compound_amount as principal * (1 + rate) ** years
# ✅ CORRECT: Use 'run' for function calls
- run maximum as max(value1, value2, value3)
- run minimum as min(score1, score2, score3)
- run formatted_amount as format_currency(total)
- run absolute_value as abs(difference)
# ✅ CORRECT: Use 'run' for REST API calls
- run api_data as rest_get("https://api.example.com/data")
- run post_result as rest_post("https://api.example.com/submit", data)
# ✅ CORRECT: Use 'run' for JSON operations
- run user_name as json_get(response, "user.name")
- run has_email as json_exists(response, "email")
- calculate item_count as json_size(response, "items")
# ❌ INCORRECT: Don't use 'calculate' for function calls
# - calculate maximum as max(value1, value2, value3) # Wrong!
# ❌ INCORRECT: Don't use 'calculate' for REST calls
# - calculate api_data as rest_get("https://api.example.com") # Wrong!# Modify existing variables
- add 10 to base_score
- subtract penalty from total_score
- multiply risk_factor by 1.5
- divide monthly_payment by 2# List manipulation
- append "HIGH_RISK" to risk_factors
- prepend "PRIORITY" to processing_flags
- remove "TEMPORARY" from account_flagsThe rule engine supports three types of loops: forEach for iterating over lists, while for conditional loops, and do-while for loops that execute at least once.
The forEach action allows you to iterate over lists and perform actions on each element.
Basic Syntax:
# Simple iteration
- forEach item in items: set total to total + item
# With index variable
- forEach item, index in items: set processedItems[index] to item * 2
# Multiple actions (separated by semicolons)
- forEach num in numbers: set temp to num * 2; calculate total as total + tempCommon Use Cases:
# Sum all values in a list
- set total to 0
- forEach amount in amounts: calculate total as total + amount
# Process each item with conditions
- set validCount to 0
- forEach score in scores: if score at_least 70 then add 1 to validCount
# Build a new list from existing data
- set doubledValues to []
- forEach value in values: append value * 2 to doubledValues
# Iterate with index for position-based logic
- set indexSum to 0
- forEach item, index in items: calculate indexSum as indexSum + index
# String concatenation
- set sentence to ""
- forEach word in words: set sentence to sentence + word + " "
# Filter and accumulate
- set evenSum to 0
- forEach num in numbers: if num % 2 equals 0 then calculate evenSum as evenSum + numAdvanced Examples:
# Multi-step processing in forEach
- set processedData to []
- forEach record in records: set temp to record * 1.1; append temp to processedData
# Nested conditions within forEach
- set highCount to 0
- set mediumCount to 0
- set lowCount to 0
- forEach score in scores: if score at_least 80 then add 1 to highCount
- forEach score in scores: if score at_least 60 and score less_than 80 then add 1 to mediumCount
- forEach score in scores: if score less_than 60 then add 1 to lowCount
# Using index for calculations
- set weightedSum to 0
- forEach value, position in values: calculate weightedSum as weightedSum + (value * position)Important Notes:
- The iteration variable (e.g.,
item) is available only within the forEach body - The index variable (if specified) starts at 0
- Multiple actions must be separated by semicolons (
;) - forEach can be nested, but keep complexity manageable for maintainability
- The list expression can be an input variable, computed variable, or expression
The while action executes actions repeatedly as long as a condition is true. The condition is checked before each iteration.
Basic Syntax:
# Simple while loop
- while counter less_than 10: add 1 to counter
# Multiple actions (separated by semicolons)
- while counter less_than 10: calculate total as total + counter; add 1 to counter
# Complex condition
- while counter less_than maxValue and total less_than 100: calculate total as total + counter; add 1 to counterCommon Use Cases:
# Count to a target value
- set counter to 0
- while counter less_than 10: add 1 to counter
# Accumulate until threshold
- set sum to 0
- set index to 0
- while sum less_than 100: calculate sum as sum + index; add 1 to index
# Process with dynamic condition
- set attempts to 0
- set success to false
- while attempts less_than 5 and success equals false: call tryOperation with []; add 1 to attempts
# Build a sequence
- set fibonacci to [0, 1]
- set count to 2
- while count less_than 10: set next to fibonacci[count - 1] + fibonacci[count - 2]; append next to fibonacci; add 1 to countImportant Notes:
- The condition is evaluated before each iteration
- If the condition is false initially, the loop body never executes
- Maximum iterations limit: 1000 (prevents infinite loops)
- Multiple actions must be separated by semicolons (
;) - The loop variable must be modified within the loop to avoid infinite loops
The do-while action executes actions at least once, then repeats as long as a condition is true. The condition is checked after each iteration.
Basic Syntax:
# Simple do-while loop
- do: add 1 to counter while counter less_than 10
# Multiple actions (separated by semicolons)
- do: calculate total as total + counter; add 1 to counter while counter less_than 10
# Complex condition
- do: set temp to value * 2; add temp to total while total less_than 100Common Use Cases:
# Execute at least once, then check condition
- set counter to 0
- do: add 1 to counter while counter less_than 5
# Process until condition met (guaranteed first execution)
- set result to 0
- do: calculate result as result + 10 while result less_than 50
# Retry logic with guaranteed first attempt
- set attempts to 0
- do: call processData with []; add 1 to attempts while attempts less_than 3
# Build data with initial value
- set values to []
- set current to 1
- do: append current to values; multiply current by 2 while current less_than 100Important Notes:
- The loop body always executes at least once, even if the condition is initially false
- The condition is evaluated after each iteration
- Maximum iterations limit: 1000 (prevents infinite loops)
- Multiple actions must be separated by semicolons (
;) - Useful when you need guaranteed first execution before checking the condition
| Loop Type | Condition Check | Minimum Executions | Use When |
|---|---|---|---|
forEach |
N/A (iterates over list) | 0 (if list is empty) | You have a list to iterate over |
while |
Before each iteration | 0 (if condition is false) | You need to check condition before executing |
do-while |
After each iteration | 1 (always executes once) | You need guaranteed first execution |
Example Comparison:
# forEach - iterates over existing list
- forEach item in [1, 2, 3]: calculate total as total + item
# while - may not execute if condition is false
- set counter to 10
- while counter less_than 5: add 1 to counter # Never executes
# do-while - always executes at least once
- set counter to 10
- do: add 1 to counter while counter less_than 5 # Executes once, then stops# Built-in functions
- call log with ["Processing started", "INFO"]
- call audit with ["Decision made", "AUDIT"]
- call notify with ["admin@company.com", "Alert message"]
# Financial functions
- call calculate_loan_payment with [amount, rate, term, "result_var"]
- call format_currency with [amount, "formatted_amount"]# Inline conditional logic
- if creditScore greater_than 750 then set tier to "PRIME"
- if annualIncome less_than 50000 then add 5 to risk_score
- if hasGuarantor equals true then subtract 10 from risk_score# Stop execution with message
- if risk_score greater_than 90 then circuit_breaker "HIGH_RISK_DETECTED"
- if fraud_indicators greater_than 3 then circuit_breaker "FRAUD_SUSPECTED"when:
# Comparison conditions
- creditScore at_least 650
- annualIncome greater_than 50000
- customerType equals "PREMIUM"
- accountStatus not_equals "SUSPENDED"
# Range conditions
- age between 18 and 65
- loanAmount not_between 0 and 1000
# String conditions
- customerName contains "Smith"
- email starts_with "admin"
- phone matches "^\\+1\\d{10}$"
# List conditions
- accountType in_list ["CHECKING", "SAVINGS"]
- riskLevel not_in_list ["HIGH", "CRITICAL"]
# Existence conditions
- exists guarantorInfo
- is_null previousLoan
- is_not_null collateralValue
# Validation conditions
- email is_email
- phone is_phone
- ssn is_ssn
- creditScore is_credit_score
- amount is_positive
- balance is_not_emptywhen:
# AND conditions
- creditScore at_least 650 AND annualIncome greater_than 50000
- age at_least 18 AND age at_most 65
# OR conditions
- creditScore at_least 750 OR hasCollateral equals true
- customerType equals "VIP" OR accountBalance greater_than 100000
# NOT conditions
- NOT (accountStatus equals "SUSPENDED")
- NOT (riskLevel in_list ["HIGH", "CRITICAL"])
# Complex combinations with parentheses
- (creditScore at_least 650 AND annualIncome greater_than 40000) OR hasGuarantor equals true
- (age at_least 21 AND employmentYears at_least 2) AND NOT (hasDelinquencies equals true)
# NEW: Validation operators in complex conditions
- (creditScore is_credit_score AND creditScore >= MIN_CREDIT_SCORE)
- (email is_email AND email is_not_empty) OR (phone is_phone AND phone is_not_empty)
- (monthlyRevenue is_positive AND monthlyExpenses is_positive AND existingDebt is_not_null)
# Multi-line complex expressions with validation operators
- (
customerName is_not_empty AND
email is_email AND
phone is_phone AND
ssn is_ssn
) AND (
creditScore is_credit_score AND
annualIncome is_positive
)conditions:
if:
and:
- compare:
left: creditScore
operator: "at_least"
right: 650
- compare:
left: annualIncome
operator: "greater_than"
right: 50000
then:
actions:
- set:
variable: "approval_status"
value: "APPROVED"
else:
actions:
- set:
variable: "approval_status"
value: "DECLINED"# Numbers
- set age to 25
- set rate to 3.5
- set amount to 1000000
# Strings
- set status to "APPROVED"
- set message to "Application processed successfully"
# Booleans
- set is_eligible to true
- set has_errors to false
# Null values
- set optional_field to null# Input variables (camelCase)
- calculate monthly_income as annualIncome / 12
# System constants (UPPER_CASE)
- when: creditScore at_least MIN_CREDIT_SCORE
# Computed variables (snake_case)
- when: debt_ratio less_than 0.4# Arithmetic operations
- calculate total as principal + interest
- calculate difference as income - expenses
- calculate product as rate * amount
- calculate ratio as numerator / denominator
- calculate remainder as amount % 100
- calculate power as base ** exponent
# Comparison operations
- when: creditScore > 700
- when: balance >= 1000
- when: age < 65
- when: score <= 850
- when: status == "ACTIVE"
- when: type != "SUSPENDED"
# String operations
- when: name contains "Smith"
- when: email starts_with "admin"
- when: phone ends_with "1234"
- when: pattern matches "^\\d{3}-\\d{2}-\\d{4}$"
# Logical operations
- when: is_eligible and has_income
- when: is_vip or has_collateral
- when: not is_suspended# Negation
- calculate negative_amount as -balance
- when: not is_active
# Existence checks
- when: exists customer_data
- when: is_null previous_loan
- when: is_not_null guarantor_info
# Validation operators in expressions (NEW FEATURE)
- set has_valid_data to (creditScore is_positive)
- set email_check to (contactEmail is_email)
- set phone_check to (phoneNumber is_phone)
- calculate data_complete as (customerName is_not_empty AND email is_email)# Mathematical functions
- run maximum as max(value1, value2, value3)
- run minimum as min(score1, score2, score3)
- run absolute as abs(difference)
- run rounded as round(decimal_value)
- run ceiling as ceil(amount)
- run floor as floor(rate)
# Financial functions
- calculate payment as calculate_loan_payment(principal, rate, term)
- calculate interest as calculate_compound_interest(principal, rate, time)
- run formatted as format_currency(amount)
# String functions
- run uppercase as upper(name)
- run lowercase as lower(email)
- run trimmed as trim(input_text)
- run name_length as length(description)
# Date/time functions
- run current_date as now()
- run today_date as today()
- run formatted_date as format_date(date_value, "yyyy-MM-dd")
- run pretty_date as format_date(date_value, "dd MMM yyyy")
- run age_years as calculate_age(birth_date) # from today
- run age_at_event as calculate_age(birth_date, event_date)
- run plus_thirty as dateadd(today_date, 30, "days")
- run days_between as datediff(start_date, end_date, "days")
# Validation functions (function-call form complements the `is_email`/`is_phone` operators)
- run email_ok as validate_email(email_address)
- run phone_ok as validate_phone(phone_number)
- run is_business_day_today as is_business_day(today_date)
- run any_check as is_valid(value, "email") # 12 known types; unknown -> error
# Null-handling & conditional helpers (DSL primitives)
- run preferred_name as coalesce(nickname, full_name, "Anonymous") # first non-null wins
- run tier as if_else(creditScore at_least 750, "PRIME", "STANDARD") # inline ternary
- run within_window as is_in_range(score, 600, 850) # function form of `between`Important: Function calls and REST/JSON expressions are only legal in
runactions and in expression contexts (function arguments, conditions, output mappings). Thecalculateaction is restricted to pure mathematical expressions (+ - * / % **) on numeric inputs -- attempting to use a function call insidecalculateraises a clean validation error.
# GET requests
- run user_data as rest_get("https://api.example.com/users/123")
- run credit_report as rest_get("https://credit-api.com/report/" + ssn)
# POST requests with body
- run api_response as rest_post("https://api.example.com/submit", request_data)
- run validation_result as rest_post("https://validator.com/check", {"email": email, "phone": phone})
# PUT requests with headers
- run update_result as rest_put("https://api.example.com/users/123", user_data, {"Authorization": "Bearer " + token})
# DELETE requests
- run delete_result as rest_delete("https://api.example.com/records/" + record_id)REST error contract: On HTTP failure (non-2xx, network error, DNS, timeout), the REST functions return a structured map:
{success: false, error: true, message: "<details>"}. Rules can branch onresponse.successto handle errors gracefully. This is intentional "chain-friendly" behaviour and is the only place in the engine where a failure does not raise an exception; everywhere else, errors propagate assuccess=falseon the rule result.
# Simple property access
- run user_name as json_get(api_response, "name")
- run user_age as json_get(api_response, "age")
# Nested property access
- run city as json_get(user_data, "address.city")
- run zip_code as json_get(user_data, "address.zipCode")
# Array access
- run first_hobby as json_get(user_data, "hobbies[0]")
- run last_transaction as json_get(account_data, "transactions[-1]")
# Array size
- calculate hobby_count as json_size(user_data, "hobbies")
- calculate transaction_count as json_size(account_data, "transactions")
# Existence checks
- run has_email as json_exists(user_data, "email")
- run has_address as json_exists(user_data, "address")
- run has_phone as json_exists(user_data, "contact.phone")All functions listed below are actually implemented in the codebase and verified against ExpressionEvaluator.java
# Basic mathematical operations
- run maximum as max(value1, value2, value3)
- run minimum as min(value1, value2, value3)
- run absolute as abs(-15.5)
- run rounded as round(3.14159)
- run ceiling as ceil(3.1)
- run floor as floor(3.9)
- run power as pow(base, exponent)
- run square_root as sqrt(16)
# Advanced math (added 26.05.08)
- run e_value as exp(1) # e^x
- run ln_value as ln(2.718) # natural log
- run log10_value as log10(1000) # base-10 log
- run sin_value as sin(0) # radians
- run cos_value as cos(0)
- run tan_value as tan(0)
- run angle as atan2(1, 1) # two-argument arc tangent
# Hashing (cryptographic digests)
- run sig as hash("payload") # SHA-256, hex-encoded
- run md5 as hash("payload", "MD5") # also SHA-1, SHA-512
# Statistical functions
- run average as avg(score1, score2, score3) # Also: average
- run sum as sum(amount1, amount2, amount3)# Case conversion
- run uppercase as upper("hello world") # Also: uppercase
- run lowercase as lower("HELLO WORLD") # Also: lowercase
# String manipulation
- run trimmed as trim(" hello ")
- run name_length as length("hello") # Also: len
- run substring as substring("hello", 1, 3) # Also: substr
- run contains_check as contains("hello", "ell")
- run starts_check as startswith("hello", "he")
- run ends_check as endswith("hello", "lo")
- run replaced as replace("hello", "l", "x")
# Templated formatting: {0}, {1}, ... substitute extra args by index.
# Wrap the whole action in YAML quotes if the template contains `: ` (colon-space).
- run greeting as format("Hello, {0}!", customerName)
- 'run msg as format("Score {0} / {1} (decision {2})", score, maxScore, decision)'
# String concatenation of N args (returns the joined string)
- run id as concat(prefix, "-", customerId, "-", suffix)# Loan and interest calculations
- run monthly_payment as calculate_loan_payment(principal, annual_rate, term_months)
- run compound_interest as calculate_compound_interest(principal, rate, time)
- run amortization as calculate_amortization(principal, rate, term)
- run apr as calculate_apr(loan_amount, fees, monthly_payment, term)
# Financial ratios and metrics
- run debt_ratio as debt_to_income_ratio(monthly_debt, monthly_income)
- run credit_util as credit_utilization(used_credit, total_credit)
- run ltv as loan_to_value(loan_amount, property_value)
- run debt_ratio_alt as calculate_debt_ratio(total_debt, total_income)
- run ltv_alt as calculate_ltv(loan_amount, property_value)
# Credit and risk scoring
- run credit_score as calculate_credit_score(payment_history, utilization, length, types, inquiries)
- run risk_score as calculate_risk_score(credit_score, income, debt_ratio)
- run payment_score as payment_history_score(payment_data)
# Utility functions
- run formatted_amount as format_currency(1234.56)
- run formatted_percent as format_percentage(0.15)
- run account_num as generate_account_number()
- run transaction_id as generate_transaction_id()# Current date/time
- run current_timestamp as now()
- run current_date as today()
- run iso_now as current_iso() # Also: now_iso() -- ISO-8601 with offset
# Date arithmetic
- run date_plus as dateadd(date_value, amount, "days") # Also: "months", "years", "weeks"
- run date_difference as datediff(start_date, end_date, "days")
- run hour_value as time_hour(timestamp)
# Date field extractors (numeric output)
- run year_num as year_of(date_value) # e.g. 2026
- run month_num as month_of(date_value) # 1..12
- run dom as day_of_month(date_value) # 1..31
- run dow as day_of_week(date_value) # ISO: Monday=1 ... Sunday=7
# Date validation / age
- run is_business_day_check as is_business_day(date_value)
- run age_check as age_meets_requirement(birth_date, min_age)
- run formatted as format_date(date_value, "yyyy-MM-dd")# Basic aggregates
- run list_size as size(my_list) # Also: count
- run list_sum as sum(number_list)
- run list_average as avg(number_list) # Also: average
- run first_item as first(my_list)
- run last_item as last(my_list)
# Ordering / dedup
- run sorted_nums as sort(my_list) # ascending; numeric or Comparable items
- run reversed_nums as reverse(my_list)
- run unique_items as distinct(my_list) # de-dupe, preserving insertion orderThe DSL has no inline-lambda syntax; filter / map / reduce / find take the
predicate or transformer as a string function name. The named function is resolved
through the same lookup the evaluator uses for any function call --
CustomFunctionRegistry first, then the built-in
catalogue -- so both engine built-ins and user-registered Spring beans work
identically.
# filter(list, function_name): keep items where the named predicate is truthy
- run large_txns as filter(transactions, "is_above_threshold")
# map(list, function_name): transform every item
- run with_fees as map(transactions, "add_fee")
# reduce(list, initial, function_name): accumulate left-to-right
# The reducer is called as fn(accumulator, item) for each item.
- run total as reduce(transactions, 0, "add_two")
# find(list, function_name): first matching item, or null if none
- run first_negative as find(balances, "is_negative_value")Tip: Register a one-arg
RuleFunctionfrom Java to act as the predicate / transformer. For numeric predicates the engine already hasis_positive,is_negative,is_zero,is_email,is_phone, etc. -- those are reachable by name too.
- run mid as median(values) # numeric median (mean of middle two on even length)
- run spread as stddev(values) # sample standard deviation (n-1 denominator)
- run var as variance(values) # sample variance
- run p95 as percentile(values, 95) # linear-interpolation percentile (p in [0,100])# Type conversions
- run as_number as tonumber("123.45") # Also: number
- run as_string as tostring(123) # Also: string
- run as_boolean as toboolean("true") # Also: boolean# Financial validation
- run is_valid_score as is_valid_credit_score(750)
- run is_valid_ssn as is_valid_ssn("123-45-6789")
- run is_valid_account as is_valid_account("1234567890")
- run is_valid_routing as is_valid_routing("021000021")
# General validation
- run is_valid_data as is_valid(value, "email")
- run in_range_check as in_range(value, min, max)# HTTP methods (all actually implemented)
- run get_response as rest_get(url)
- run post_response as rest_post(url, body)
- run put_response as rest_put(url, body, headers)
- run delete_response as rest_delete(url, headers)
- run patch_response as rest_patch(url, body, headers)
- run api_response as rest_call(method, url, body, headers)# JSON path operations (all actually implemented)
- run value as json_get(json_object, "path.to.property") # Also: json_path
- run exists as json_exists(json_object, "optional.property")
- run size as json_size(json_object, "array_property")
- run type as json_type(json_object, "property")# Distance and location
- run distance as distance_between(lat1, lon1, lat2, lon2)
# Data security
- run encrypted as encrypt(data, key)
- run decrypted as decrypt(encrypted_data, key)
- run masked as mask_data(sensitive_data, mask_pattern)
# Advanced financial calculations
- run payment_schedule as calculate_payment_schedule(principal, rate, term)# coalesce: first non-null wins (NULL-coalescing default)
- run preferred_name as coalesce(nickname, full_name, "Anonymous")
# if_else: inline ternary expression (avoids a full `if/then/else` action block)
- run tier as if_else(creditScore at_least 750, "PRIME", "STANDARD")
- run discount as if_else(membership equals "GOLD", 0.20, 0.05)
# is_in_range: function form of the `between` operator (inclusive both ends)
- run score_band_ok as is_in_range(score, 600, 850)# Audit and logging (all actually implemented)
- call audit with ["Decision made", "AUDIT"]
- call audit_log with ["Rule executed", "TRACE"]
- call send_notification with ["recipient", "message"]
# First-class logging action -- routes through SLF4J at the named level
- run echoed as log("Rule fired for applicant " + applicantId, "INFO")
- call log with ["debug snapshot", "DEBUG"] # action-context invocationSupported levels (case-insensitive): TRACE, DEBUG, INFO (default), WARN, ERROR. The function returns its message so it can be chained inside expressions.
invoke_rule(code, ...) evaluates another stored rule by code and returns its output map. Inputs are passed as alternating "key", value pairs trailing the rule code -- this avoids the YAML/JSON {} flow-mapping ambiguity that bites authors who try to write inline maps in action lines.
then:
# No inputs
- run health as invoke_rule("system_health_check")
# Two-argument form (only when the second argument is already a Map in context)
- run score as invoke_rule("scoring_rule", existing_input_map)
# Alternating-pairs form (recommended for inline literals)
- run underwriting as invoke_rule("composite_underwriting",
"creditScore", creditScore,
"annualIncome", annualIncome,
"existingDebt", existingDebt)
- set tier to underwriting.tier
- set approved to underwriting.approvedThe nested rule evaluation is synchronous (blocking on the engine's boundedElastic worker). If the invoked rule fails to parse, the rule code does not exist, or the nested evaluation reports success=false, invoke_rule raises an error that propagates as a failed result of the outer rule -- consistent with the fail-loud contract.
⚠️ invoke_rulerequires aRuleInvokerbean. The default Spring auto-configuration wiresRuleInvokerImpl(backed byRuleDefinitionService) automatically; outside Spring you can supply your own implementation.
Register your own functions in a Spring @Bean and call them from any rule:
@Configuration
class MyRulesConfig {
@Bean
CommandLineRunner registerCustomFunctions(CustomFunctionRegistry registry) {
return args -> {
registry.register("regional_risk", a ->
Set.of("CA", "NY").contains(a[0]) ? 10 : 0);
registry.register("fraud_score", a ->
fraudService.score(String.valueOf(a[0])));
};
}
}# Then use them like any built-in function:
when:
- fraud_score(applicantId) at_most MAX_FRAUD_SCORE
then:
- run risk_bump as regional_risk(region)
- run is_clean as fraud_score(applicantId) less_than 50Resolution order: Custom functions are checked before the built-in catalog -- if you
register a function with the same name as a built-in (e.g., max), your function wins.
Names are matched case-insensitively. The same registered function is reachable from both
expression contexts (run / calculate arg / condition) and action contexts (call).
The DSL has no top-level circuit_breaker: config block. Resilience and early
termination are expressed as an action inside a rule's then: block:
then:
- if risk_score at_least 90 then circuit_breaker "HIGH_RISK_DETECTED"
- set processing_status to "OK" # never executes if the previous action triggeredWhen the action fires, the engine stops the rule cleanly. The result reports
success=true with circuitBreakerTriggered=true and the message above; any
already-set variables remain in the output, but no subsequent actions run.
A decision_table: block expresses a multi-row decision as a table of input
predicates and output assignments. This is the most concise way to encode rules
that boil down to "look at columns X, Y, Z; return outputs A, B" -- the same shape
that drools/DMN solve.
name: "Auto Insurance Premium Table"
description: "Tier and rate by credit and age"
inputs:
creditScore: number
age: number
decision_table:
inputs: [creditScore, age]
outputs: [tier, rate]
hit_policy: FIRST
rules:
- when:
- creditScore at_least 750
- age between 25 and 65
then:
tier: "PRIME"
rate: 3.0
- when:
- creditScore at_least 650
then:
tier: "PREFERRED"
rate: 5.0
- otherwise: true
then:
tier: "STANDARD"
rate: 9.0
output:
tier: tier
rate: rateHit policies -- how the engine picks which rows contribute:
| Policy | Behavior |
|---|---|
FIRST |
First matching row wins. Default. |
ANY |
Any matching row's outputs apply; the engine uses the first match. |
UNIQUE |
Exactly one row must match. Multiple matches -> rule fails with a clean diagnostic. |
COLLECT |
Each output column is collected into a list of values from every matching row. |
Output values: numbers, booleans, lists, and maps pass through unchanged. Strings are taken as literals by default. Prefix a string with = to mark it as a DSL expression evaluated against the current context:
then:
tier: "PRIME" # literal string
rate: 3.0 # literal number
computed_premium: "= basePremium * 1.5" # expression -- result of basePremium * 1.5
band: "= if_else(score at_least 700, \"HIGH\", \"LOW\")"This rule shape is mutually exclusive with when:/then:, conditions:, and rules: at the same level. If decision_table: is present it takes precedence.
Declare an upper bound on a rule's wall-clock runtime to protect callers from runaway loops, slow REST calls, or pathological data:
name: "Risk Assessment"
timeout: 5s # also accepts "500ms" or a raw number of milliseconds
when:
- creditScore at_least 600
then:
- run report as rest_get("https://slow.example.com/risk")
- set assessed to trueExceeding the timeout fails the rule cleanly: success=false with an error like
Rule 'Risk Assessment' exceeded its declared timeout of 5000ms. No partial outputs
escape; the engine relies on Reactor's Mono.timeout() to cancel the work.
metadata:
tags: ["credit", "risk-assessment", "banking"]
author: "Risk Management Team"
category: "Credit Scoring"
priority: "HIGH"
last_modified: "2025-01-15"
review_date: "2025-06-15"constants:
- code: MIN_CREDIT_SCORE
defaultValue: 650
- code: MAX_DEBT_RATIO
defaultValue: 0.4
- code: PREMIUM_THRESHOLD
defaultValue: 100000name: "Mixed Syntax Example"
description: "Demonstrates mixing simple and complex syntax"
inputs: [creditScore, annualIncome]
# Simple syntax for main logic
when:
- creditScore at_least 600
then:
- set initial_approval to true
# Complex conditional action within simple syntax
- if annualIncome greater_than 75000 then set tier to "PREMIUM"
- if annualIncome between 50000 and 75000 then set tier to "STANDARD"
- if annualIncome less_than 50000 then set tier to "BASIC"
# Complex syntax for detailed conditions
conditions:
if:
and:
- compare:
left: initial_approval
operator: "equals"
right: true
- compare:
left: tier
operator: "not_equals"
right: "BASIC"
then:
actions:
- set:
variable: "final_decision"
value: "APPROVED"name: "Comprehensive Credit Assessment"
description: "Full credit evaluation with risk scoring"
version: "2.1.0"
metadata:
tags: ["credit", "risk", "banking"]
author: "Risk Management Team"
category: "Credit Scoring"
inputs:
- creditScore
- annualIncome
- monthlyDebt
- employmentYears
- requestedAmount
- hasCollateral
constants:
- code: MIN_CREDIT_SCORE
defaultValue: 650
- code: MAX_DEBT_RATIO
defaultValue: 0.4
when:
- creditScore at_least MIN_CREDIT_SCORE
- annualIncome greater_than 40000
- employmentYears at_least 1
then:
# Calculate key financial ratios
- calculate debt_to_income as monthlyDebt / (annualIncome / 12)
- calculate loan_to_income as requestedAmount / annualIncome
# Determine base approval
- set base_approved to true
# Risk scoring with conditional logic
- if creditScore at_least 750 then set credit_risk_score to 10
- if creditScore between 700 and 749 then set credit_risk_score to 20
- if creditScore between 650 and 699 then set credit_risk_score to 30
# Income risk assessment
- if annualIncome at_least 100000 then set income_risk_score to 5
- if annualIncome between 60000 and 99999 then set income_risk_score to 15
- if annualIncome between 40000 and 59999 then set income_risk_score to 25
# Calculate final risk score
- calculate total_risk_score as credit_risk_score + income_risk_score
# Adjust for collateral
- if hasCollateral equals true then subtract 10 from total_risk_score
# Final decision logic
- if total_risk_score less_than 20 then set approval_tier to "PRIME"
- if total_risk_score between 20 and 35 then set approval_tier to "STANDARD"
- if total_risk_score greater_than 35 then set approval_tier to "SUBPRIME"
# Set final decision
- if debt_to_income less_than MAX_DEBT_RATIO then set final_decision to "APPROVED"
- if debt_to_income at_least MAX_DEBT_RATIO then set final_decision to "REVIEW_REQUIRED"
else:
- set base_approved to false
- set final_decision to "DECLINED"
- set rejection_reason to "Does not meet minimum requirements"
output:
base_approved: boolean
debt_to_income: number
loan_to_income: number
total_risk_score: number
approval_tier: text
final_decision: text
rejection_reason: textname: "Customer Data Enrichment"
description: "Fetch and process customer data from external APIs"
version: "1.0.0"
inputs:
- customerId
- requiresValidation
when:
- customerId is_not_null
- customerId is_not_empty
then:
# Fetch customer data from external API
- run customer_data as rest_get("https://api.customer-service.com/customers/" + customerId)
# Extract customer information using JSON paths
- run customer_name as json_get(customer_data, "personalInfo.fullName")
- run customer_email as json_get(customer_data, "contactInfo.email")
- run customer_phone as json_get(customer_data, "contactInfo.phone")
- run credit_score as json_get(customer_data, "creditInfo.score")
# Check if additional data exists
- run has_employment_info as json_exists(customer_data, "employmentInfo")
- run has_address as json_exists(customer_data, "addressInfo")
# Conditional processing based on available data
- if has_employment_info equals true then run annual_income as json_get(customer_data, "employmentInfo.annualIncome")
- if has_address equals true then run zip_code as json_get(customer_data, "addressInfo.zipCode")
# Validation if required
- if requiresValidation equals true then run email_valid as validate_email(customer_email)
- if requiresValidation equals true then run phone_valid as validate_phone(customer_phone)
# Set processing status
- set data_enrichment_complete to true
- run processing_timestamp as now()
else:
- set data_enrichment_complete to false
- set error_message to "Invalid or missing customer ID"
output:
customer_name: text
customer_email: text
customer_phone: text
credit_score: number
annual_income: number
zip_code: text
email_valid: boolean
phone_valid: boolean
data_enrichment_complete: boolean
processing_timestamp: text
error_message: textname: "Multi-Stage Loan Processing"
description: "Sequential processing with multiple validation stages"
version: "1.0.0"
inputs:
- applicantData
- loanAmount
- loanTerm
rules:
- name: "Initial Validation"
when:
- exists applicantData
- loanAmount greater_than 0
- loanTerm greater_than 0
then:
- set validation_stage_1 to "PASSED"
- run applicant_age as json_get(applicantData, "age")
- run applicant_income as json_get(applicantData, "annualIncome")
else:
- set validation_stage_1 to "FAILED"
- circuit_breaker "INVALID_APPLICATION_DATA"
- name: "Age and Income Verification"
when:
- validation_stage_1 equals "PASSED"
- applicant_age at_least 18
- applicant_income greater_than 25000
then:
- set validation_stage_2 to "PASSED"
- calculate monthly_income as applicant_income / 12
- calculate loan_to_income_ratio as loanAmount / applicant_income
else:
- set validation_stage_2 to "FAILED"
- set rejection_reason to "Age or income requirements not met"
- name: "Risk Assessment"
when:
- validation_stage_2 equals "PASSED"
- loan_to_income_ratio less_than 5.0
then:
- set risk_assessment to "LOW"
- run estimated_monthly_payment as calculate_loan_payment(loanAmount, 0.05, loanTerm)
- set pre_approval_status to "APPROVED"
else:
- set risk_assessment to "HIGH"
- set pre_approval_status to "REQUIRES_REVIEW"
- name: "Final Processing"
when:
- pre_approval_status equals "APPROVED"
then:
- set final_status to "APPROVED"
- run approval_timestamp as now()
- 'call log with ["Loan approved for amount: " + loanAmount, "INFO"]'
else:
- set final_status to "DECLINED"
- 'call log with ["Loan declined - " + rejection_reason, "INFO"]'
output:
validation_stage_1: text
validation_stage_2: text
applicant_age: number
monthly_income: number
loan_to_income_ratio: number
risk_assessment: text
estimated_monthly_payment: number
pre_approval_status: text
final_status: text
approval_timestamp: text
rejection_reason: textThis example exercises:
- Validation operators in
set ... to (...)expressions (is_email,is_phone,is_credit_score,is_positive,is_not_null,is_not_empty) - Multi-line boolean composition with
and/or(lower-case keywords are canonical;AND/ORare accepted as case-insensitive aliases) if_else(condition, then, else)as the inline-ternary replacement -- the DSL does not support C-style? :- Sub-rules with shared state -- variables set in earlier rules are visible to later ones
name: "B2B Credit Scoring with Enhanced Validation"
description: "Demonstrates validation operators in complex expressions"
version: "2.1.0"
inputs:
- monthlyRevenue
- monthlyExpenses
- existingDebt
- monthlyDebtPayments
- verifiedAnnualRevenue
- creditScore
- customerName
- email
- phone
- ssn
constants:
- code: MIN_BUSINESS_CREDIT_SCORE
defaultValue: 650
- code: EXCELLENT_CREDIT_THRESHOLD
defaultValue: 750
rules:
- name: "Data Validation Stage"
when:
- exists monthlyRevenue
- exists creditScore
then:
- set has_complete_financial_data to (monthlyRevenue is_positive and monthlyExpenses is_positive and existingDebt is_not_null and monthlyDebtPayments is_positive and verifiedAnnualRevenue is_positive)
- set has_valid_contact_info to (customerName is_not_empty and email is_email and phone is_phone and ssn is_ssn)
- set has_valid_credit_data to (creditScore is_credit_score and creditScore >= MIN_BUSINESS_CREDIT_SCORE)
- name: "Financial Analysis"
when:
- has_complete_financial_data equals true
- has_valid_credit_data equals true
then:
- set meets_credit_requirements to (creditScore is_credit_score and creditScore >= MIN_BUSINESS_CREDIT_SCORE and (creditScore >= EXCELLENT_CREDIT_THRESHOLD or verifiedAnnualRevenue >= 500000))
- calculate debt_to_income_ratio as existingDebt / verifiedAnnualRevenue
- calculate monthly_cash_flow as monthlyRevenue - monthlyExpenses - monthlyDebtPayments
- set has_positive_cash_flow to (monthly_cash_flow is_positive)
- name: "Final Decision"
when:
- has_valid_contact_info equals true
- meets_credit_requirements equals true
- has_positive_cash_flow equals true
then:
- set final_decision to "APPROVED"
# Score each factor with if_else() (no C-style ternary), then sum.
- run credit_score_pts as if_else(creditScore >= EXCELLENT_CREDIT_THRESHOLD, 40, 20)
- run cash_flow_pts as if_else(monthly_cash_flow >= 10000, 30, 15)
- run dti_pts as if_else(debt_to_income_ratio <= 0.3, 30, 10)
- calculate approval_score as credit_score_pts + cash_flow_pts + dti_pts
else:
- set final_decision to "DECLINED"
- set decline_reasons to []
- if has_valid_contact_info equals false then append "Invalid contact information" to decline_reasons
- if meets_credit_requirements equals false then append "Credit requirements not met" to decline_reasons
- if has_positive_cash_flow equals false then append "Insufficient cash flow" to decline_reasons
output:
has_complete_financial_data: boolean
has_valid_contact_info: boolean
has_valid_credit_data: boolean
meets_credit_requirements: boolean
debt_to_income_ratio: number
monthly_cash_flow: number
has_positive_cash_flow: boolean
final_decision: text
approval_score: number
decline_reasons: list- AST-Based: All syntax is parsed into strongly-typed Abstract Syntax Tree nodes
- Expression-Driven: Actions and conditions support complex nested expressions
- Type-Safe: Automatic type inference and validation during parsing
- Error Handling: Comprehensive error reporting with source location information
- Validation Operators in Expressions: Validation operators like
is_positive,is_email,is_phone, etc. can now be used in complex expressions, not just simple conditions - Complex Boolean Expressions: Multi-line boolean expressions with AND/OR operators and parentheses are fully supported in both conditions and expressions
- Enhanced ExpressionParser: The ExpressionParser now supports validation operators as unary operations, enabling natural syntax like
(email is_email AND phone is_phone) - Unified Operator Support: Both ConditionParser and ExpressionParser now support the same set of validation operators for consistent syntax across the DSL
- BETWEEN Operator Improvements: Fixed parsing issues with
BETWEENoperator to properly handlevalue between min and maxsyntax - Age Operators in Expressions:
age_at_leastandage_less_thanoperators now work correctly in both conditions and expressions
- AST Caching: Parsed rule models are cached for improved performance
- Lazy Evaluation: Expressions are evaluated only when needed
- Connection Pooling: R2DBC connection pools are optimized for high-load scenarios
- Batch Operations: Support for bulk rule evaluation capabilities
- Syntax Validation: Complete DSL syntax validation during rule definition
- Runtime Validation: Type checking and constraint validation during execution
- Circuit Breaker: Built-in resilience patterns for external service calls
- Audit Trail: Comprehensive logging of all rule operations and decisions
- Validation Operators in Expressions: All validation operators (
is_positive,is_email,is_phone,is_credit_score, etc.) can now be used in complex expressions - Multi-line Boolean Expressions: Support for complex boolean expressions with AND/OR operators and parentheses in both conditions and expressions
- Enhanced Parser Architecture: ExpressionParser now supports validation operators as unary operations for consistent syntax
- BETWEEN Operator Fixes: Resolved parsing conflicts where ExpressionParser was consuming AND tokens needed by BETWEEN operator
- Age Operator Support: Added proper support for
age_at_leastandage_less_thanoperators in both expression and condition contexts - Real-world Testing: Validated with comprehensive B2B credit scoring scenarios including multi-stage evaluation workflows
- Complex boolean expressions with validation operators in
setactions - Multi-line expressions with proper parentheses grouping
- Validation operators in conditional expressions
- Mixed validation and comparison operators in complex conditions
- New primitives:
coalesce(...),if_else(cond, then, else),is_in_range(value, low, high). - New built-ins:
calculate_age(birth[, asOf]),format_date(date[, pattern]),validate_email(value),validate_phone(value)(function-call form complements existingis_email/is_phoneoperators). - Extension point:
CustomFunctionRegistrySpring bean lets applications register their ownRuleFunctionimplementations callable from rules. - Error contract: the engine now fails loud by design rather than silently swallowing errors. See the table below.
- Sub-rule action parity: sub-rules under
rules:now accept the same map-shaped actions as the top level (e.g., YAML-collapsed- forEach x in xs: ...).
| Situation | Old (pre-26.05) | New |
|---|---|---|
| Unknown function name | log.warn + null |
IllegalArgumentException -> rule reports success=false |
| Non-numeric string in arithmetic | silently coerced to ZERO | IllegalArgumentException naming the operand |
Bad regex pattern in matches |
false |
IllegalArgumentException naming the pattern |
| Missing bean property | log.warn + null |
IllegalArgumentException naming class + property (maps still get null on missing key) |
Unknown is_valid(value, type) type |
false |
IllegalArgumentException listing supported types |
Unknown dateadd/datediff unit |
log.warn + null |
IllegalArgumentException listing supported units |
| Action throws during execution | logged + next action runs | Rule reports success=false with action index + cause |
| Condition throws during evaluation | silently flips to else branch | Rule reports success=false with the real cause |
circuit_breaker action triggered |
(unchanged) | Rule reports success=true with circuitBreakerTriggered=true |
| REST function HTTP failure | structured error map (unchanged) | Structured error map (unchanged -- intentional chain-friendly form) |
set var to null (e.g., json_get missing) |
NPE caught silently | Variable stored as null; rule succeeds |
Ready to put this knowledge into practice?
- 🚀 Start Simple: Try examples from Quick Start Guide
- 🎯 Find Patterns: Browse Common Patterns Guide for your use case
- 🏛️ Set Standards: Review Governance Guidelines for team practices
- 💡 Practice: Work through B2B Credit Scoring Tutorial
- 🏗️ Understand Architecture: Read Architecture Guide
- ⚡ Optimize: Check Performance Optimization
This guide is based on comprehensive analysis of the actual Firefly Framework Rule Engine AST-based parser implementation. All syntax examples and features documented here are verified against the codebase implementation and tested with real-world scenarios.