Rule Engine in Python

Introduction to rule engines

A rule engine is a software system that separates business logic from application code, allowing you to define, manage, and execute business rules without modifying your core application. Think of it as a specialized decision-making component that evaluates conditions and takes actions based on those evaluations.

The key advantage of rule engines is that they allow you to express complex business logic in a declarative way, focusing on what should happen rather than how it should be implemented. This makes your business rules more accessible, maintainable, and adaptable to changing requirements.

In traditional programming, business logic is often embedded within application code, making it difficult to modify without risking unintended consequences. A rule engine solves this problem by providing a clear separation between:

  • Business rules: The conditions and actions that define your business policies
  • Application code: The technical implementation that powers your systems

This separation enables business analysts and domain experts to understand and even modify rules without needing deep programming knowledge, while developers can focus on building robust application infrastructure.

So why else does it make sense to use a rule engine?

  • Centralized business logic

    Rules are defined and stored in a central location, making them easier to manage, update, and reuse across different parts of your application. This centralization ensures consistency in how rules are applied throughout your systems.

  • Reduced complexity

    By extracting business rules from your application code, you simplify both components. Your application code becomes cleaner and more focused on technical concerns, while your business rules become more explicit and easier to understand.

  • Making your APIs more flexible

    When business requirements change (and they always do), you can update rules without modifying and redeploying your application code. This significantly reduces the time and risk associated with implementing changes.

  • Easier collaboration between devs and business

    Rule engines bridge the gap between business and technical teams. Your business people can easily understand, review, validate, or contribute to rule definitions, while you, as a developer, can focus on the technical implementation.

  • Stronger oversight

    With rules explicitly defined and centrally managed, it's easier to track what rules exist, when they were changed, and why. This is of particular importance for compliance and governance in regulated industries.

Real-world rule engine use cases

Rule engines are most useful in scenarios where complex decision-making logic needs to be applied consistently across many transactions or processes. Here are three real-world examples from different industries:

Telecommunications: Customer service routing

A telecom provider needs to route customer service calls to the appropriate department based on multiple factors.

When a customer calls customer service:

  1. If they are a premium customer and have an open billing dispute, route to the VIP billing resolution team
  2. If they have called 3+ times about the same issue within 48 hours, route to the escalation team
  3. Otherwise, route to the standard customer service queue


Implementing these rules in code would create a series of if-else statements that would be difficult to maintain as routing policies evolve. With a rule engine, these policies can be defined declaratively and modified without changing the underlying call routing system.

Airports: Flight delay management

An airport operations system needs to determine what actions to take when flights are delayed.

When a flight is delayed:

  1. If the delay is > 3 hours and it's an international flight, offer vouchers to all passengers
  2. If the delay is > 5 hours and it's the last flight of the day, offer hotel accommodations
  3. If the delay affects a connecting flight, automatically rebook the passenger on the next available flight


These rules involve multiple conditions and actions that may change seasonally or based on airport policies. Instead of keeping them in a configuration database that will be difficult to understand by business people, the rules can be kept in a human-readable form now.

Government: Permit approval process

A municipal government needs to determine whether building permit applications can be automatically approved or require manual review.

When a permit application is submitted:

  1. If it's for a residential property and the proposed changes are within zoning limits and no historical preservation rules apply, approve automatically
  2. If it requires environmental impact assessment or affects protected land, route to environmental review committee
  3. If the applicant has had permit violations in the past 3 years, flag for supervisor review


These rules involve combinations of conditions that would be cumbersome to implement and maintain in regular code. A rule engine allows the government to easily update approval criteria as regulations change.

How do rules look like?

Here are the same airport rules that we talked about earlier.

You'll immediately note that it's pretty much regular English all throughout. It's also easy to recognize that it sort of reminds one of the Python syntax, and that's on purpose too.

# #########################################################################################

rule
    Flight_Delay_Handling_International
docs
    """ Manages passenger compensation and rebooking for delayed flights.
    Used for ensuring proper customer service during flight disruptions.
    """
defaults
    international_voucher_threshold_hours = 3
when
    flight_delay_hours > international_voucher_threshold_hours and
    flight_type == 'international'
then
    offer_vouchers = True
    voucher_amount = 50
    notify_all_passengers = True

# #########################################################################################

rule
    Flight_Delay_Handling_Last_Flight_Of_Day
docs
    """ Manages passenger compensation and rebooking for delayed flights.
    Used for ensuring proper customer service during flight disruptions.
    """
defaults
    hotel_accommodation_threshold_hours = 5
when
    flight_delay_hours > hotel_accommodation_threshold_hours and
    is_last_flight_of_day == True
then
    offer_hotel_accommodation = True
    offer_meal_vouchers = True
    arrange_transportation = True

# #########################################################################################

rule
    Flight_Delay_Handling_Connecting_Fligts
docs
    """ Manages passenger compensation and rebooking for delayed flights.
    Used for ensuring proper customer service during flight disruptions.
    """
when
    affects_connecting_flight == True and
    (is_baggage_checked is False or next_flight_within_hours <= 24)
then
    auto_rebook_next_available = True
    notify_passenger = True
    update_baggage_routing = True

# #########################################################################################

So who is the Zato Rule Engine for?

It's designed for people like you, who:

  • Are Python programmers that frequently integrate, automate or build APIs and backend systems
  • Often discuss various rules and logic with business colleagues and need a clean way to show exactly how things will work, in a manner that will be easy to understand both by you and the business person
  • Look for a distraction-free way to express complex conditions that would otherwise require too much programming

How to install the Zato Rule Engine?

  • The rule engine is an integral part of Zato, so you don't need to install it separately
  • If you've already installed Zato, then the rule engine is already running and you can start using it
  • If not yet, head over to the installation documentation, and you'll have Zato installed in under 5 minutes
  • Or you can use this blueprint project to set up a complete environment in a few minutes as well

VS Code plugin

You can install a VS Code plugin for syntax highlighting - go to Extensions, search for "Zato Rules" and click "Install".

VS Code Plugin for Zato Rule Engine

Where are rule files kept?

  • Once a Zato container is running, you'll find demo rules in your container's /opt/zato/env/qs-1/server1/config/repo/user-conf directory and you can map it to your host (e.g. your WSL2 Ubuntu server under Windows) - the best way to automate it is use the blueprint project
  • All rules are saved in files with an extension of .zrules, e.g. telco_bss.zrules, gov.zrules or airport.zrules
  • Each time you create or edit such a file, it will be automatically read by the engine - you don't need to restart anything

Zato Rule Engine Demo Rules

Using the Zato Rule Engine

Let's create a simple rule file for our flight delay scenario. Create or modify the file named airport.zrules so it reads as below:

# #########################################################################################

rule
    01_Flight_Delays
docs
    """ Determines what actions to take when flights are delayed based on delay duration
    and passenger status. Used by the flight management system to automate
    passenger notifications and service offerings.
    """
defaults
    flight_delay = 0
    passenger_type = 'regular'
    is_international = False
when
    flight_delay > 120 and
    passenger_type == 'diamond' and
    is_international == True
then
    send_notification = True
    offer_accomodation = True
    template = 'DelayAlert'

# #########################################################################################

rule
    02_Flight_Cancellations
docs
    """ Determines what actions to take when flights are cancelled.
    Provides different handling based on passenger status and flight type.
    """
when
    is_cancelled == True and
    (passenger_type == 'diamond' or passenger_type == 'platinum')
then
    send_notification = True
    offer_rebooking = True
    offer_accomodation = True
    template = 'CancellationVIP'

# #########################################################################################

The primary interface for using rules is through the self.rules.match method of your Zato services, which allows you to match data against defined rules and take actions based on the results.

Here's a simple example of using the rule engine in a service:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class FlightDelayHandler(Service):

    def handle(self) -> 'None':

        # Input data about the flight delay situation
        data = {
            'flight_delay': 150,          # Delay in minutes
            'passenger_type': 'diamond',  # Frequent flyer status
            'is_international': True,     # International flight flag
        }

        # Evaluate a specific rule
        if result := self.rules.airport_01_Flight_Delays.match(data):

            # Access the 'then' values to determine actions
            send_notification = result.then['send_notification']
            offer_accomodation = result.then['offer_accomodation']
            template = result.then['template']

            self.logger.info('Sending notification: {send_notification}')
            self.logger.info('Offering accommodation: {offer_accomodation}')
            self.logger.info('Template to use: {template}')

This service checks if the current flight delay situation matches the conditions defined in the airport_01_Flight_Delays rule. If there's a match, it accesses the rule's then values to determine what actions to take.

Rule syntax and structure

Each rule in a .zrules file follows a specific structure with several sections:

Rule name

The rule section defines the name of the rule. This should be a unique identifier within the container (derived from the filename).

All rules are always evaluated alphabetically so if you'd like to be specific about what has priority, you can use a prefix number for ordering (e.g., 01_, 02_) followed by a descriptive name.

rule
    01_Flight_Delays

Note that in your Python code you refer to rules using their full name, which is a concatenation of the .zrules file name plus the actual rule name. If the rule above is in an airport.zrules name then its full name is airport_01_Flight_Delays.

Documentation

The docs section provides human-readable documentation about the rule's purpose and usage. This is important for maintainability, especially when rules are managed by multiple people.

docs
    """ Determines what actions to take when flights are delayed based on delay duration
    and passenger status. Used by the flight management system to automate passenger
    notifications and service offerings.
    """

Defaults (optional)

The defaults section defines default values for variables that might not be present in the input data. This makes rules more robust by handling missing data gracefully.

defaults
    flight_delay = 0
    passenger_type = 'regular'
    is_international = False

Conditions

The when section defines the conditions that must be met for the rule to match. This is where you specify the business logic using comparison operators, logical operators, and variable references.

when
    flight_delay > 120 and
    passenger_type == 'diamond' and
    is_international == True

Actions

The then section defines the actions or values that should be returned when the rule matches. These can be simple values or complex objects, such as dicts or lists.

then
    send_notification = True
    offer_accomodation = True
    template = 'DelayAlert'
    status = {'status_type':'alert', 'status_subtype':'delay'}
    notify_list = [123, 456, 789]

Advanced rule conditions

Zato's rule engine supports a variety of operators and expressions for creating sophisticated rule conditions. Here are some advanced techniques:

Logical operators

You can combine multiple conditions using logical operators:

  • and: Both conditions must be true
  • or: At least one condition must be true
  • not: Negates a condition
when
    (flight_delay > 180 or is_cancelled == True) and
    not (passenger_type == 'basic')

Comparison operators

The rule engine supports all standard comparison operators:

  • ==: Equal to
  • !=: Not equal to
  • >, >=: Greater than, greater than or equal to
  • <, <=: Less than, less than or equal to

Membership testing

You can check if a value is in a list using the in operator:

when
    aircraft_type in ['B777', 'B787', 'A330', 'A350', 'A380', 'B747'] and
    gate_width_meters >= 45

Regular expressions

For pattern matching, you can use the =~ operator with regular expressions:

when
    flight_number =~ "[A-Z]{2}\d{3,4}" and
    destination_airport == "KE'

This matches flight numbers that consist of two uppercase letters followed by 3-4 digits.

What if I need for rules to do more?

Remember that the rule engine is a built-in capability of Zato, and you access your rules via Python services.

It means that you can continue to use your services to access any kind of APIs, read or update data in databases and so on, while your rules make decisions only.

It's by design that the syntax of rules is kept very simple and it's used to express decision logic only. Any kind of interactions with the outside world is done with your Zato services in Python, which gives you a very clean separation between the decision making and acting on the decisions made.

Using rules in Zato services

Zato provides several ways to use rules in your services, depending on your specific needs. Let's explore the different patterns:

Matching against a specific rule

The most straightforward approach is to match data against a specific rule:

class TelecomCustomerService(Service):

    def handle(self) -> 'None':

        # Customer data
        data = {
            'customer_tier': 'premium',
            'open_tickets': 2,
            'account_age_days': 365,
        }

        # Match against a specific rule
        if self.rules.telecom_01_Premium_Support.match(data):

            # Rule matched, take appropriate action
            self.logger.info('Routing customer to premium support')

Accessing rule results

When you need to use the values defined in the rule's then section, capture the result object:

class AirportGateAssignment(Service):

    def handle(self) -> 'None':

        # Aircraft data
        data = {
            'aircraft_type': 'B787',
            'gate_width_meters': 50,
            'jetbridge_length_meters': 15,
            'gate_currently_available': True,
        }

        # Match and capture the result
        if result := self.rules.airport_09_GateAssignment_WidebodyAircraft.match(data):

            # Access the 'then' values
            assigned_gate = result.then['assigned_gate']
            priority_boarding = result.then['priority_boarding']

            self.logger.info('Gate {assigned_gate} with priority boarding: {priority_boarding}')

Matching against all rules in a file

You can match data against all rules in a file and use the first matching rule:

class ResponseCoordinator(Service):

    def handle(self) -> 'None':

        # Incident data
        data = {
            'severity': 9,
            'unit_affected': 'ABC',
        }

        # Match against all rules in the 'emergency.zrules' file
        if result := self.rules.emergency.match(data):

            # The first matching rule's results are returned
            priority_level = result.then['priority_level']
            response_units = result.then['response_units']

            self.logger.info('Priority: {priority_level}, dispatching: {response_units}')

Matching against multiple specific rules

You can also match against a specific set of rules:

class TelecomBillingService(Service):

    def handle(self) -> 'None':

        # Customer billing data
        data = {
            'account_type': 'business',
            'usage_gb': 250,
            'contract_months': 24,
        }

        # Rules to check
        rules = [
            'telecom_03_Business_Discount',
            'telecom_05_Volume_Discount',
            'telecom_08_Loyalty_Program'
        ]

        # Match against the specified rules
        if result := self.rules.match(data, rules=rules):
            discount_percentage = result.then['discount_percentage']
            self.logger.info('Applied discount: {discount_percentage}%')

Using dynamic rule names

Sometimes you may need to determine which rule to use at runtime:

class PermitProcessor(Service):

    def handle(self) -> 'None':

        # Permit application data
        data = {
            'zoning_code': 'residential',
            'proposed_height_feet': 30,
            'proposed_setback_feet': 20,
        }

        # Determine which rule to use based on our input
        permit_type = self.request.payload.permit_type
        permit_type = permit_type.capitalize()

        # Build a full name of the rule
        rule_name = f'government_07_PermitApproval_{permit_type}'

        # Match against the dynamically selected rule
        if result := self.rules[rule_name].match(data):

            permit_status = result.then['permit_status']
            self.logger.info('Permit status: {permit_status}')

Rule matching and result handling

When you match data against rules, there are several patterns for handling the results, depending on your business requirements.

Simple boolean matching

The simplest approach is to use the match result as a boolean condition:

if self.rules.telecom_01_Premium_Support.match(data):

    # Rule matched, take appropriate action
    self.route_to_premium_support()

Accessing match results

When you need to use the values defined in the rule's then section, capture the result object:

if result := self.rules.airport_01_Flight_Delays.match(data):

    # Access the 'then' values
    send_notification = result.then['send_notification']
    offer_accommodation = result.then['offer_accomodation']
    template = result.then['template']

    # Use these values to drive your business logic
    if send_notification:
        self.send_customer_notification(template)

    if offer_accommodation:
        self.arrange_hotel_accommodation()

Handling no matches

It's important to handle cases where no rules match:

# A rule matched, use its results
if result := self.rules.government.match(data):
    self.process_according_to_rule(result)

# No rules matched, use default handling
else:
    self.apply_default_processing()

Cascading rules

Sometimes you need to apply multiple rules in sequence, with each rule potentially modifying the data:

class ComplexRuleProcessor(Service):

    def handle(self) -> 'None':

        # Initial data
        data = {
            'customer_id': '12345',
            'transaction_amount': 500,
        }

        # Apply eligibility rules
        if eligibility_result := self.rules.eligibility.match(data):

            # Update data based on eligibility result
            data['eligible_for_discount'] = eligibility_result.then['is_eligible']

            # Now apply pricing rules with the updated data
            if pricing_result := self.rules.pricing.match(data):
                final_amount = pricing_result.then['final_amount']
                self.logger.info('Final transaction amount: {final_amount}')

Best practices for rule management

As your rule-based system grows, following these best practices will help maintain clarity and efficiency:

Rule organization

  • Group related rules in the same file: Rules that belong to the same business domain should be kept together.

  • Use consistent naming conventions: Prefix rule names with numbers (e.g., 01_, 02_) to control evaluation order within a container.

  • Create separate containers for different domains: For example, keep telecom_rules.zrules, airport_rules.zrules, and government_rules.zrules as separate files.

Rule documentation

  • Always include detailed docs: Each rule should have clear documentation explaining its purpose, when it applies, and what actions it triggers.

  • Document the business context: Include information about why the rule exists and who is responsible for its business logic.

  • Update documentation when rules change: Keep the documentation synchronized with the actual rule logic.

Rule design

  • Keep rules focused - each rule should address a specific business scenario rather than trying to handle multiple cases.

  • Use defaults for robustness - define default values for variables that might be missing from input data.

  • Order conditions by complexity - put simpler, more frequently false conditions first to take advantage of short-circuit evaluation, and take advantage of other performance tuning opportunities

Rule testing

  • Create test cases for each rule - ensure that each rule works correctly with various input data. You'll be invoking your rules from Python services so you can use the Zato's API testing in pure English framework for that.

  • Test rule interactions - verify that rules work correctly when applied in sequence or combination.

  • Validate rule changes - when modifying rules, test both the new behavior and ensure existing scenarios still work correctly.

Deployment and versioning

  • Version control your rule files - keep rule files in a version control system alongside your application code (e.g. in git).

  • Deploy rules with care - treat rule changes like code changes, with proper review and testing before deployment.

Performance tuning

  • You can influence the performance of the rule engine by authoring rules with a few specific principles in mind. Check the dedicated chapter on performance tuning for details.

Summary

The Zato Rule Engine provides a clean way to implement business rules in your integration services in a manner that is easy to understand by both technical and business people.

By separating business logic from application code, you gain flexibility, maintainability, and clarity in your systems, so you can make them more adaptable to changing business requirements while keeping your code clean and focused on technical concerns.