Advanced FEEL Usage Patterns¶
Introduction¶
The Friendly Enough Expression Language (FEEL) provides powerful capabilities for expressing business logic in DMN models. While beginners often use simple expressions and individual variables, advanced DMN practitioners leverage FEEL's more sophisticated features to create cleaner, more maintainable decision models.
This guide explores advanced usage patterns for FEEL, focusing on structured data, complex operations, and efficient design practices.
Working with Structured Data¶
Context Expressions¶
Contexts (also called structures) are one of FEEL's most powerful features. A context is a collection of key-value pairs, similar to a JSON object or dictionary in other programming languages.
When to Use Contexts¶
Use contexts when:
- Grouping related data: Instead of having separate variables for firstName, lastName, and age, create a single Person context.
- Creating intermediate calculations: Break complex formulas into named steps.
- Returning multiple values: When a decision needs to return several related values.
- Organizing complex logic: Use contexts to create a clear structure for multi-step logic.
Basic Context Example¶
{
baseAmount: 1000,
discountRate: 0.10,
discountAmount: baseAmount * discountRate,
netAmount: baseAmount - discountAmount
}
This context calculates a discounted amount in clear, self-documenting steps. The result of this expression is the final entry (netAmount = 900).
Nested Contexts¶
Contexts can be nested to represent hierarchical data:
{
customer: {
name: "John Smith",
category: "Premium",
contact: {
email: "john@example.com",
phone: "555-1234"
}
},
order: {
id: "ORD-12345",
date: date("2023-06-15"),
total: 1250.00
},
discount: if customer.category = "Premium" then 0.15 else 0.05,
finalAmount: order.total * (1 - discount)
}
Access nested values using dot notation: customer.contact.email
.
Context as Intermediate Calculation¶
When logic becomes complex, using contexts to break calculations into steps improves readability:
{
// Calculate mortgage payment components
loanAmount: requestedAmount * (1 + pointsPercent/100),
monthlyRate: annualRate/12/100,
numberOfPayments: term * 12,
// Calculate monthly payment using standard formula
payment: loanAmount *
(monthlyRate * (1 + monthlyRate) ^ numberOfPayments) /
((1 + monthlyRate) ^ numberOfPayments - 1),
// Calculate additional values
totalPayments: payment * numberOfPayments,
totalInterest: totalPayments - loanAmount
}
Lists and Collections¶
Lists in FEEL are ordered collections of items, which can be of the same or different types. They're powerful for handling multiple values, iterating, and filtering.
When to Use Lists¶
Use lists when:
- Working with multiple similar items: Products, transactions, accounts, etc.
- Performing calculations across multiple entries: Totals, averages, etc.
- Filtering and selecting subsets of data: Finding all items meeting certain criteria.
- Iterating over a collection: Applying the same logic to each item.
Creating Lists¶
// Simple list of numbers
[1, 2, 3, 4, 5]
// List of strings
["apple", "banana", "cherry"]
// List of contexts
[
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
]
Accessing List Items¶
In FEEL, list indices start at 1, not 0:
// Get the first item
employees[1].name // "Alice"
// Get the last item
employees[-1].name // "Charlie"
List Filtering¶
One of FEEL's most powerful features is the ability to filter lists:
// Get all employees older than 25
employees[age > 25] // Returns a list with Alice and Charlie
// Get employees with names starting with 'A'
employees[starts with(name, "A")] // Returns a list with Alice
List Iteration with For Expressions¶
The for
expression allows you to apply logic to each item in a list:
// Calculate the square of each number
for n in [1, 2, 3, 4, 5] return n * n // [1, 4, 9, 16, 25]
// Calculate ages after 5 years
for person in employees return {
name: person.name,
currentAge: person.age,
futureAge: person.age + 5
}
Quantified Expressions¶
FEEL provides some
and every
expressions for checking conditions across list items:
// Is any employee over 30?
some employee in employees satisfies employee.age > 30 // true
// Are all employees under 40?
every employee in employees satisfies employee.age < 40 // true
Tables in FEEL¶
A table in FEEL is essentially a list of contexts with the same structure. This pattern is incredibly useful for representing tabular data in decision models.
Creating and Working with Tables¶
Consider a table of loan products:
// A table of loan products
[
{ lender: "Bank A", rate: 4.5, term: 30, minAmount: 100000 },
{ lender: "Bank B", rate: 4.2, term: 30, minAmount: 150000 },
{ lender: "Bank C", rate: 4.8, term: 15, minAmount: 50000 },
{ lender: "Bank D", rate: 4.0, term: 30, minAmount: 200000 }
]
Filtering Tables¶
You can filter tables based on any column:
// Find all 30-year loans
loanProducts[term = 30]
// Find loans with rate < 4.5% and minimum amount <= 150000
loanProducts[rate < 4.5 and minAmount <= 150000]
Extracting Single Values from Filtered Tables¶
When filtering returns a single row, you can extract specific values:
Note the [1]
at the end - this extracts the value from the collection result of the filter.
Processing Table Data¶
You can combine filtering with functions to analyze table data:
// Find the lowest rate among 30-year loans
min(loanProducts[term = 30].rate)
// Find the average minimum loan amount
mean(loanProducts.minAmount)
Advanced FEEL Patterns¶
Pattern: Decision Tables with Context Results¶
Instead of returning simple values from decision tables, return contexts for richer information:
// Decision table output with context
{
category: "Premium",
discountRate: 0.15,
nextReviewDate: date("2024-06-30")
}
This enables a single decision table to return multiple related values rather than requiring separate decisions.
Pattern: Dynamic Property Access¶
When property names are determined at runtime, use FEEL's context access features:
// Access property dynamically
product[(if region = "US" then "domesticPrice" else "internationalPrice")]
Pattern: Lookup Tables¶
Create and use lookup tables for reference data:
// Tax rates by region
{
taxRates: [
{ region: "Northeast", rate: 0.065 },
{ region: "Southeast", rate: 0.055 },
{ region: "Midwest", rate: 0.060 },
{ region: "West", rate: 0.075 }
],
// Look up tax rate for the customer's region
applicableTaxRate: taxRates[region = customer.region].rate[1]
}
Pattern: Calculation Pipeline¶
Create a sequence of transformations using contexts:
{
// Input data
basePrice: 100,
quantity: 5,
// Calculation pipeline
subtotal: basePrice * quantity,
discountRate: if quantity >= 5 then 0.1 else 0,
discountAmount: subtotal * discountRate,
afterDiscount: subtotal - discountAmount,
taxRate: 0.07,
taxAmount: afterDiscount * taxRate,
total: afterDiscount + taxAmount
}
Pattern: Field Aggregation¶
Calculate aggregate values across a collection:
{
// Calculate total order value
orderItems: [
{ product: "A", price: 10, quantity: 2 },
{ product: "B", price: 15, quantity: 1 },
{ product: "C", price: 5, quantity: 4 }
],
// Calculate individual line totals
lineTotals: for item in orderItems return item.price * item.quantity,
// Calculate order total
orderTotal: sum(lineTotals)
}
Pattern: Hierarchical Categorization¶
Use nested if-expressions in a context for complex categorization:
{
// Determine loan risk category
creditScore: applicant.creditScore,
debtToIncome: applicant.monthlyDebt / applicant.monthlyIncome,
// Hierarchical risk assessment
riskCategory:
if creditScore < 600 then "High Risk"
else if creditScore < 700 then
if debtToIncome > 0.36 then "Medium-High Risk"
else "Medium Risk"
else
if debtToIncome > 0.42 then "Low-Medium Risk"
else "Low Risk"
}
Best Practices for Using Structured Data in FEEL¶
1. Define Clear Data Structures¶
Create custom data types for your structured data in the DMN model:
- Define a
Customer
type with fields like name, id, category - Define a
LoanProduct
type with fields like rate, term, minAmount - Define collection types like
collection of LoanProduct
2. Use Meaningful Names¶
Name your context entries clearly to document their purpose:
// Poor naming
{ a: 100, b: a * 0.1, c: a - b }
// Good naming
{ baseAmount: 100, discountAmount: baseAmount * 0.1, netAmount: baseAmount - discountAmount }
3. Break Complex Logic into Steps¶
Don't try to do everything in one expression:
// Too complex
if applicant.creditScore >= 700 and applicant.monthlyDebt / applicant.monthlyIncome < 0.36 and applicant.employmentYears > 2 then "Approved" else "Declined"
// Better approach
{
creditAcceptable: applicant.creditScore >= 700,
debtRatioAcceptable: applicant.monthlyDebt / applicant.monthlyIncome < 0.36,
employmentStable: applicant.employmentYears > 2,
approved: creditAcceptable and debtRatioAcceptable and employmentStable,
result: if approved then "Approved" else "Declined"
}
4. Reuse Common Logic¶
Extract repeated calculations into business knowledge models (BKMs):
// Business Knowledge Model: Calculate Payment
function(principal, rate, term) {
monthlyRate: rate/12/100,
numberOfPayments: term * 12,
payment: principal *
(monthlyRate * (1 + monthlyRate) ^ numberOfPayments) /
((1 + monthlyRate) ^ numberOfPayments - 1)
}
5. Properly Handle Empty Lists¶
When filtering lists, be prepared for empty results:
// This could return an empty list
customers[customerID = "12345"]
// Safely access a property with null handling
if count(customers[customerID = "12345"]) = 0
then "Customer not found"
else customers[customerID = "12345"].name[1]
Practical Examples¶
Example 1: Loan Qualification Analysis¶
{
// Input data
loanAmount: 250000,
applicantIncome: 75000,
applicantDebt: 1500,
creditScore: 720,
// Calculate key ratios
debtToIncome: applicantDebt * 12 / applicantIncome,
loanToIncome: loanAmount / applicantIncome,
// Evaluate criteria
incomeAdequate: applicantIncome >= 50000,
debtRatioAcceptable: debtToIncome <= 0.36,
loanRatioAcceptable: loanToIncome <= 4,
creditAcceptable: creditScore >= 680,
// Determine qualification
qualified: incomeAdequate and debtRatioAcceptable and loanRatioAcceptable and creditAcceptable,
// Generate detailed result
result: {
qualified: qualified,
failedCriteria: [
if not incomeAdequate then "Income below minimum" else null,
if not debtRatioAcceptable then "Debt-to-income ratio too high" else null,
if not loanRatioAcceptable then "Loan amount too high for income" else null,
if not creditAcceptable then "Credit score below minimum" else null
][item != null],
recommendedLoanAmount: if not loanRatioAcceptable then applicantIncome * 4 else loanAmount
}
}
Example 2: Product Recommendation Engine¶
{
// Customer and product data
customer: {
id: "C12345",
segment: "Premium",
recentPurchases: ["Laptop", "Headphones", "Monitor"],
totalSpent: 2500
},
productCatalog: [
{ id: "P1", name: "Laptop", category: "Computers", price: 1200 },
{ id: "P2", name: "Smartphone", category: "Electronics", price: 800 },
{ id: "P3", name: "Headphones", category: "Audio", price: 300 },
{ id: "P4", name: "Monitor", category: "Computers", price: 400 },
{ id: "P5", name: "Keyboard", category: "Accessories", price: 100 },
{ id: "P6", name: "Mouse", category: "Accessories", price: 50 },
{ id: "P7", name: "Tablet", category: "Electronics", price: 600 }
],
// Recommendation logic
purchasedCategories: for product in productCatalog[name = customer.recentPurchases] return product.category,
recommendedProducts: productCatalog[
// Product is not already purchased
not(list contains(customer.recentPurchases, name)) and
// Product is in a category the customer buys
list contains(purchasedCategories, category) and
// Product is within reasonable price range (not more than 50% of total spent)
price <= customer.totalSpent * 0.5
],
topRecommendations:
if count(recommendedProducts) > 3
then recommendedProducts[1..3]
else recommendedProducts
}
Example 3: Financial Report Analysis¶
{
// Financial data
financialData: [
{ year: 2021, quarter: 1, revenue: 1200000, expenses: 950000 },
{ year: 2021, quarter: 2, revenue: 1350000, expenses: 1050000 },
{ year: 2021, quarter: 3, revenue: 1100000, expenses: 900000 },
{ year: 2021, quarter: 4, revenue: 1500000, expenses: 1150000 },
{ year: 2022, quarter: 1, revenue: 1400000, expenses: 1100000 },
{ year: 2022, quarter: 2, revenue: 1600000, expenses: 1200000 }
],
// Calculate key metrics
yearlyData: {
"2021": {
totalRevenue: sum(financialData[year = 2021].revenue),
totalExpenses: sum(financialData[year = 2021].expenses),
profit: sum(financialData[year = 2021].revenue) - sum(financialData[year = 2021].expenses),
quarters: financialData[year = 2021]
},
"2022": {
totalRevenue: sum(financialData[year = 2022].revenue),
totalExpenses: sum(financialData[year = 2022].expenses),
profit: sum(financialData[year = 2022].revenue) - sum(financialData[year = 2022].expenses),
quarters: financialData[year = 2022]
}
},
// Calculate growth metrics
revenueGrowth: (yearlyData."2022".totalRevenue - yearlyData."2021".totalRevenue) / yearlyData."2021".totalRevenue,
profitGrowth: (yearlyData."2022".profit - yearlyData."2021".profit) / yearlyData."2021".profit,
// Generate analysis
analysis: {
revenueGrowthRate: revenueGrowth * 100,
profitGrowthRate: profitGrowth * 100,
revenuePerformance: if revenueGrowth > 0.15 then "Excellent" else if revenueGrowth > 0.05 then "Good" else "Needs Improvement",
profitPerformance: if profitGrowth > 0.2 then "Excellent" else if profitGrowth > 0.1 then "Good" else "Needs Improvement",
recommendations: [
if revenueGrowth < 0.1 then "Evaluate sales strategies" else null,
if profitGrowth < 0.15 then "Review cost structure" else null,
if yearlyData."2022".quarters[-1].revenue < yearlyData."2022".quarters[-2].revenue then "Investigate Q2 revenue decline" else null
][item != null]
}
}
Conclusion¶
Advanced FEEL usage patterns with contexts, lists, and tables enable you to create more maintainable, powerful, and expressive DMN models. By organizing your decision logic using these structured data approaches, you can:
- Improve clarity: Make complex logic easier to understand
- Enhance maintainability: Structure your logic for easier updates
- Create reusable components: Build modular decision logic
- Handle complex scenarios: Express sophisticated business rules naturally
As you develop DMN models, consider how these patterns can be applied to your specific business domains. Start with simple structures and gradually incorporate more advanced patterns as your comfort with FEEL increases.
Remember that the goal is not to create the most sophisticated expressions possible, but to express business logic clearly and maintainably. Always prioritize readability and maintainability over clever but hard-to-understand expressions.