Skip to content
🚀 Play in Aletyx Sandbox to start building your Business Processes and Decisions today! ×

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:

  1. Grouping related data: Instead of having separate variables for firstName, lastName, and age, create a single Person context.
  2. Creating intermediate calculations: Break complex formulas into named steps.
  3. Returning multiple values: When a decision needs to return several related values.
  4. 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:

  1. Working with multiple similar items: Products, transactions, accounts, etc.
  2. Performing calculations across multiple entries: Totals, averages, etc.
  3. Filtering and selecting subsets of data: Finding all items meeting certain criteria.
  4. 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:

// Find Bank B's rate
loanProducts[lender = "Bank B"].rate[1]

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:

  1. Improve clarity: Make complex logic easier to understand
  2. Enhance maintainability: Structure your logic for easier updates
  3. Create reusable components: Build modular decision logic
  4. 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.