SESL logo light
Login

SESL Language Reference & Engine Architecture

The comprehensive guide to the SESL language specification, engine implementation, and rule evaluation model. Learn the syntax, structure, and internal architecture that powers deterministic, explainable decision logic.

1. Introduction to SESL

SESL (Simple Expert System Language) is a deterministic, structured rule language designed for expressing decision logic in a form that is both human-readable and machine-executable. Unlike probabilistic systems or general-purpose programming languages, SESL is intentionally focused: it allows you to represent business rules, policies, and decision workflows as clear, declarative statements that the SESL engine evaluates using forward-chaining inference.

A SESL model is a single YAML-based text document that contains rules, example scenarios (facts), and optional constants. The engine reads this model and applies the rules to input data to produce deterministic outcomes with complete explainability. Every decision is backed by a detailed trace showing exactly which rules fired, in what order, and why each conclusion was reached.

1.1 Purpose and design philosophy

The core purpose of SESL is to separate your business decision logic from your application code while maintaining complete transparency, traceability, and the ability to change rules without complex deployments. Instead of embedding decision logic deep inside application code, SESL lets you express it as clear, structured rules that can be understood, reviewed, and modified by business stakeholders as easily as by software engineers.

SESL embraces four fundamental design principles that shape everything about how the engine works and how you write rules.

The first principle is Determinism. This means that whenever you run the engine with the same input data, it will always produce exactly the same output. There is no randomness, no probabilistic guessing, and no hallucination. You know what the engine will do because you wrote the rules and you can see exactly how they execute. This is critical for compliance, auditing, and for systems where consistency and reproducibility matter more than anything else.

The second principle is Explainability. Every decision made by SESL is backed by a complete trace showing exactly which rules fired, in what order, and why each conclusion was reached. This enables auditing, regulatory compliance verification, and most importantly, gives your users confidence that decisions are fair, transparent, and based on documented rules they can understand and challenge.

The third principle is Readability. Rules are expressed in a simple, structured text format that can be read and discussed by both non-technical business stakeholders and software engineers. You do not need to learn a programming language to understand what a SESL rule does. Business analysts can read a rule and understand it. Regulators can read it and verify compliance. This makes rules documents and rules review much more practical.

The fourth principle is Maintainability. Business logic can be added, removed, and reordered without changing the engine itself. When you need to update a rule or add a new decision path, you edit the model file. Changes are localized to one place, reducing the risk of unintended side effects that often come from large code changes. You can version control your rules the same way you version control your code, and rules changes do not require full application deployments.

1.2 Main use cases

SESL works particularly well for scenarios where decision logic needs to be understood, audited, and changed frequently. Here are the situations where SESL makes the most sense.

If non-technical users need to understand or review the rules that drive decisions, SESL is ideal. Because the language is readable and structured, business analysts and subject matter experts can review rules without needing technical support.

When decisions must be explained to regulators, auditors, customers, or internal review teams, SESL provides complete traceability. You can show exactly which rules were evaluated and why a particular decision was made. This is essential in regulated industries like banking, insurance, and healthcare.

If your decision logic changes frequently and you need to edit rules without large code deployments, SESL eliminates the coupling between business logic and application code. Rules can be updated independently, tested, and deployed without requiring application restarts or code reviews.

When compliance and reproducibility are critical requirements, SESL ensures that the same input always produces the same output. There is no guessing about what the system will do in a particular situation. This certainty is invaluable for compliance audits and regulatory demonstrations.

Finally, when multiple teams need to collaborate on rule definitions without code-level integration risks, SESL provides a neutral format that business teams, compliance teams, and engineering teams can all work with. Rules become a shared artifact that belongs to the organization rather than the engineering team.

1.3 Typical SESL applications

Across many industries, organizations use SESL to make decisions that matter. Here are the most common applications.

In Eligibility and Underwriting, SESL helps determine whether customers qualify for loans, insurance products, or benefits. When a customer applies for a loan, SESL can evaluate their income, credit history, employment status, and other factors to decide whether they are eligible and what terms apply. Insurance underwriting uses similar logic to decide which policies can be issued and at what premium rates.

In Risk Assessment and Scoring, SESL evaluates multiple factors to produce a risk score or risk band. A bank might use SESL to score the credit risk of a loan applicant. An insurance company might score the operational risk of a new business partnership. An investment firm might score the compliance risk of a transaction. These risk scores then drive business decisions about approval, pricing, or enhanced review.

In Pricing and Discounting, SESL applies volume discounts, dynamic pricing rules, and premium calculations based on customer characteristics and purchase patterns. An e-commerce company might use SESL to determine discounts based on customer tier and purchase quantity. An insurance company might use SESL to calculate premiums based on risk factors.

In Compliance and Policy Enforcement, SESL ensures that transactions and processes comply with regulatory requirements and internal policies. Banks use SESL for sanctions screening and regulatory compliance checks. Government agencies use SESL to apply policy rules consistently across all constituents. Healthcare providers use SESL to enforce clinical guidelines and billing rules.

In Operational Automation, SESL makes routing decisions, prioritization decisions, and workflow selection decisions. A customer service system might use SESL to route inquiries to the right department based on the nature of the inquiry. A logistics company might use SESL to decide which warehouse should fulfill an order based on location and inventory.

In Fraud Detection, SESL identifies suspicious transactions and patterns with clear evidence trails. A payment processor might use SESL to flag transactions that meet suspicious criteria like unusually large amounts, unusual timing, or unusual geography. Because every flag is backed by which rules matched, investigation teams can understand exactly why something was flagged.

1.4 Comparison with alternative approaches

When you face a decision problem, you have several options for how to solve it. SESL compares favorably to each alternative in different ways, depending on what matters most to your organization.

Compared to hard-coded decision logic embedded directly in your application, SESL provides several advantages. When business logic is hard-coded, changing it requires code review, testing, and deployment of new application binaries. This coupling between business logic and application code makes changes slow and risky. SESL rules are declarative and separate from your application. A rule change does not require code review or application deployment. Business teams can propose rule changes, review them with domain experts, and deploy them independently. This separation of concerns dramatically reduces the friction and risk of rule updates.

Compared to general-purpose rules engines that attempt to be comprehensive, SESL is intentionally small and focused. Large rules engines often support complex features like nested rule sets, stateful queries, and full Turing-completeness. This power comes at the cost of complexity. SESL intentionally does not attempt to be Turing-complete. This deliberate simplicity makes the engine easier to understand, easier to audit for safety and correctness, and easier to explain to stakeholders. You can reason about what SESL will do because it is fundamentally simpler than a general-purpose engine.

Compared to machine learning models, SESL is fundamentally different in philosophy. Machine learning models learn patterns from data and generate probabilistic outputs. The same input may produce different outputs depending on probabilistic weights or random seeds. SESL is deterministic and fully explainable. The same input always produces the same output. SESL is ideal for situations where reproducibility and explainability are non-negotiable requirements. Machine learning excels when you have lots of historical data and you want to discover patterns. SESL excels when you have documented decision rules and you want to apply them consistently and transparently.

Compared to spreadsheets, which many organizations use to manage complex logic, SESL provides structure, type safety, and version control. Spreadsheets are flexible and accessible, but they often become unmaintainable as they grow. Hidden dependencies, circular references, and untracked changes become common. SESL enforces structure through its syntax. It supports dependency analysis to catch problems before they happen in production. Rules can be version controlled in your standard version control system, with full change history and audit trails. For complex decision logic that multiple people need to maintain, SESL is much more maintainable than spreadsheets.

1.5 Structure of this guide

This comprehensive reference guide is designed to take you from understanding what SESL is all the way through to writing, validating, deploying, and maintaining SESL models in production. The guide is organized into 17 major sections, each building on the previous ones.

If you are completely new to SESL, start with Sections 1 through 5. Section 1 is this introduction, which gives you the conceptual foundation. Section 2 explains what SESL is used for and how it fits into your architecture. Section 3 covers installation so you have the engine running. Section 4 is a quick start that walks you through creating your first tiny model and running it. Section 5 explains the overall structure of a SESL model so you understand the big picture before diving into details.

Once you understand the basics, Sections 6 through 9 provide a complete reference for the SESL language itself. Section 6 explains how to structure rules and what fields each rule can contain. Section 7 covers the complete syntax for conditions and actions. Section 8 is a comprehensive reference for expressions, operators, and built-in functions. Section 9 explains how to structure your input data (facts) and how the engine uses them.

Sections 10 through 14 dive into the engine itself and advanced features. Section 10 explains all the configuration options that control how strictly the engine checks your code and handles edge cases. Section 11 covers validation and linting, which help you catch mistakes early. Section 12 explains how the engine works internally: how it loads models, orders rules, and executes them. Section 13 explains the output the engine produces and how to interpret it. Section 14 covers truth maintenance, which enables you to explain exactly why a particular value was produced.

Finally, Sections 15 through 17 provide practical guidance. Section 15 includes five realistic business examples showing SESL solving actual decision problems in finance, retail, logistics, and compliance. Section 16 covers best practices for organizing your models, naming conventions, and common pitfalls to avoid. Section 17 wraps up with next steps and guidance on how to continue learning and building with SESL.

Throughout this guide, code examples use YAML syntax, which is the standard format for SESL models. When showing how to use SESL from code, we use Python, which is the language in which SESL is implemented. Even if you primarily use SESL from a different language, the Python examples show the same concepts that apply in any language.

2. What SESL is used for

SESL is designed for decision problems where you need to apply a set of rules to structured input data and produce decisions or scores. The typical pattern is that your rules read input facts, evaluate conditions, and write conclusions to result fields. If this pattern matches your problem, SESL is likely a good fit.

2.1 Types of problems SESL solves well

SESL is well-suited for several broad categories of decision problems. Understanding which category your problem falls into helps you understand whether SESL is the right choice.

Binary decisions are situations where there are only two possible outcomes, such as approve or decline, pass or fail, or flag or clear. A loan application is approved or declined. A compliance check passes or fails. A transaction is flagged for review or cleared. Many business processes boil down to binary decisions, and SESL handles these elegantly.

Multi-level classifications are situations where there are multiple possible outcomes organized into categories or bands. Instead of just approve or decline, you might have approve, refer, or decline. Instead of safe or risky, you might have low risk, medium risk, or high risk. SESL can apply rules to populate these classifications based on input conditions.

Numeric calculations that depend on conditions are situations where the output value depends on the values of the inputs. You might apply a 10 percent discount to large orders, a 15 percent discount to very large orders, and an additional loyalty discount on top. SESL rules can compute these tiered calculations based on conditions.

Aggregation of signals into a single score or recommendation are situations where multiple factors contribute to a final score. A credit score might aggregate income, debt level, payment history, and employment stability. A risk score might aggregate multiple risk factors. SESL makes it easy to express how these component factors combine into a final output.

2.2 How SESL fits into your overall architecture

SESL is not designed to be your entire application. Instead, it is a decision component that sits alongside your existing application services. Understanding how SESL fits into your architecture helps you design the interface between SESL and your code.

The typical workflow is straightforward. First, your application gathers input data from users, databases, or external services and organizes it into a structured facts structure. Think of facts as the input to the SESL engine. Second, your application loads a SESL model from a model file, a database, or a configuration service. The model contains the rules that will be evaluated. Third, your application calls the SESL engine, passing in the facts and the model. The engine evaluates all the rules against the facts. Fourth, the engine returns the updated facts, which now include result fields with the decisions, scores, and explanations. Finally, your application takes those results and uses them to drive your user experience or downstream actions. You might show the decision to the user, send it to another system, store it in a database, or trigger automated workflows based on it.

This architecture means that your application code does not need to contain any decision logic. All of that logic lives in the SESL model, which business teams can understand and modify. Your application code simply prepares input data, calls the engine, and processes output results.

3. Installation instructions

Getting SESL installed on your system is straightforward and quick. SESL is distributed as a single downloadable zip file that contains the SESL executable and example files. You can be up and running in just a few minutes. This section walks you through the installation process, testing your installation, and managing upgrades and removals.

3.1 Prerequisites

Before you download and install SESL, make sure you have the basic prerequisites in place. The good news is that SESL has minimal requirements.

You need a computer running Windows, macOS, or Linux with enough disk space to store the SESL executable and your model files. Most machines have plenty of space; a few hundred megabytes is sufficient.

You need internet access to download the zip file from https://www.sesl.ai. You also need to be able to unzip files on your computer. Most operating systems have built-in tools for this (right-click on a zip file and select "Extract" on Windows, or double-click on macOS, or use the unzip command on Linux).

For local development and learning, you need a text editor to write model files. Any editor works—Visual Studio Code, Sublime Text, Notepad, vim, or whatever you prefer. No special IDE is required.

For production use, you may want a version control system like Git to manage your model files, and you may want to set up automated testing or deployment pipelines to manage model updates safely.

3.2 Downloading SESL

SESL is distributed as a single zip file available from https://www.sesl.ai. Downloading and installing SESL is as simple as downloading a file.

Follow these steps:

  1. Visit https://www.sesl.ai in your web browser.
  2. Look for the download link for SESL. This will typically say something like "Download SESL" or "Get SESL."
  3. Click the download link. Your browser will download a zip file to your Downloads folder (or your default download location).
  4. Navigate to the location where the zip file was downloaded.
  5. Unzip the file. On Windows, right-click and select "Extract All." On macOS, double-click the file. On Linux, use the command unzip sesl-*.zip.
  6. A directory will be created containing the SESL executable and example files. Remember this location—you will need it to run SESL.

That's it! You have successfully installed SESL. Unlike software that requires a complex installer, SESL simply lives in the directory you extracted it to. There is no registration, no system-wide installation, and no hidden configuration files to worry about.

3.3 Testing your installation

After installation, you should verify that SESL is working correctly. SESL provides two simple ways to test your installation.

The easiest way is to start SESL in online session mode. Navigate to the directory where you extracted SESL and run the SESL executable. On most systems, you can simply run:

./sesl

(On Windows, use sesl.exe instead.) This will start SESL in interactive mode, where you can explore the interface and verify that everything is working. When you run SESL this way, you will also see information about the time remaining on your binary. SESL binaries are time-limited (typically valid for a certain number of days), and you can see how many days are available by checking the information displayed when SESL starts in online mode.

Alternatively, you can test SESL in batch mode using a command-line option. This approach is useful for automated testing or for running SESL in a script. Run a command like:

./sesl --batch my_model.sesl.yaml

This will load your model file and run it in batch mode, printing results to the console. If both of these methods work without errors, your SESL installation is ready to use.

3.4 Upgrading SESL

When a new version of SESL is released with bug fixes or new features, upgrading is simple. Since SESL is a standalone zip file with no complex installation, you simply download the new version and replace the old directory.

To upgrade to a new version:

  1. Back up your model files and any important data (just to be safe).
  2. Download the new SESL zip file from https://www.sesl.ai, following the same process as the initial installation.
  3. Extract the new zip file to a new directory (or replace the old directory if you are sure you have backed up your data).
  4. Update any scripts or configuration that might reference the old SESL directory location.
  5. Test the new version using the same testing steps described in section 3.3.

Your model files will work with the new version—SESL maintains backward compatibility across updates, so you do not need to modify your models when upgrading.

3.5 Uninstalling SESL

If you need to remove SESL from your system for any reason, the process is simple since SESL is self-contained in a single directory.

To uninstall SESL:

  1. Make sure you have copied any model files or important data from the SESL directory to another location (if you want to keep them).
  2. Simply delete the SESL directory. On Windows, right-click and select "Delete." On macOS or Linux, use the command rm -rf sesl-directory-name.
  3. That's it! SESL is completely removed from your system. There are no lingering files, registry entries, or configuration to clean up.

Unlike complex software installations, SESL leaves no traces on your system when you remove it. This makes it easy to try SESL risk-free or to maintain multiple versions for testing purposes.

3.6 Understanding SESL binary time limits

Each SESL binary is time-limited, meaning it has an expiration date after which it will no longer work. This is a normal part of SESL's licensing model. When you download a version of SESL, it is valid for a certain number of days from the release date (typically 30, 60, or 90 days depending on your license).

You can see how many days your SESL binary has remaining by starting SESL in online mode and checking the information displayed. It will tell you something like "This SESL binary expires in 45 days" or "This SESL binary is valid for 30 days from the release date."

When your SESL binary expires, you simply download a new version from https://www.sesl.ai to get a fresh binary with more days available. This update model keeps your SESL installation current and ensures you are always using a supported version. Upgrading is simple and painless—just download, extract, and go.

4. Quick start guide

This quick start section walks you through a complete, concrete example from beginning to end. We will create a tiny SESL model, load it, run the engine, and see the results. The goal is not to cover all possible features or all the edge cases, but to give you a practical sense of what working with SESL feels like and how the pieces fit together.

If you are following along, make sure you have SESL installed (see Section 3). You will need a text editor and a Python interpreter. Plan to spend about 10 minutes working through this example.

4.1 Create a minimal model file

Start by creating a new text file on your computer called loan-example.sesl.yaml. Open this file in your text editor and paste the following content into it. This tiny model contains everything needed for a working SESL model: a name, some constants, a couple of rules, and an example scenario to test against.

model: "Loan eligibility example"

meta:
  author: "Example author"
  created: "2025-01-01"
  source: "Internal demo"

const:
  minimum_income: 25000
  high_income: 60000

rules:
  - rule: "Set high income flag"
    if: "applicant.income >= const.minimum_income"
    then:
      result.high_income: true
    reason: "Applicant meets the minimum income threshold"

  - rule: "Recommend approval for very high income"
    priority: 10
    if: "applicant.income >= const.high_income"
    then:
      result.decision: "approve"
      result.decision_reason: "Very high income"
    reason: "Applicant has very high income"

facts:
  - name: "Applicant A"
    applicant:
      income: 65000
    result: {}

This model contains two rules and one fact scenario:

  • The first rule sets a flag when the income meets the minimum.
  • The second rule decides to approve the loan for very high income.

4.2 Running the model using SESL online

SESL provides an interactive online mode that lets you load and test models right from your terminal or command prompt. This is the quickest way to explore a model and see what the rules produce. You interact with SESL through a command-line interface that guides you through loading your model and running the scenarios you have defined.

To start SESL in interactive mode, navigate to the directory where you extracted SESL and where your model file is located. Then simply run the SESL executable with no arguments:

./sesl

(On Windows, use sesl.exe instead of ./sesl.)

When you run this command, SESL will start and display information about itself, including how many days the binary is valid for. The interactive interface will then ask you to load a model. You will be prompted to provide the path to your model file. Type the name of your model file:

Load model:  loan-example.sesl.yaml

SESL will parse your model and show you the available scenarios defined in the facts section. It will then ask you which scenario you want to run. Select a scenario by its number or name, and SESL will execute the rules against the scenario's facts, displaying the results and explanations on screen.

This interactive mode is excellent for development, debugging, and understanding how your rules work. You can quickly load different models and test scenarios without writing any code. When you are ready to move to testing or production, you can use the batch mode or Python API described below.

4.3 Running the model using --batch mode

When you need to run SESL non-interactively—for example, as part of an automated workflow, a build pipeline, or a scheduled task—you can use batch mode. Batch mode loads your model, runs all the defined scenarios, and outputs the results to the console without requiring any interactive input.

To run your model in batch mode, use the --batch flag and provide the path to your model file:

./sesl --batch loan-example.sesl.yaml

SESL will load the model, execute all the scenarios defined in the facts section, and print the results to the console. Each scenario will be processed in order, and you will see the output for each one. This mode is perfect for integration into scripts, continuous integration systems, or any situation where you need automated, unattended execution.

Batch mode is particularly useful when you are testing a collection of models or scenarios, or when you want to capture the output for logging or analysis. The results are printed in a structured format that you can parse programmatically if needed.

4.4 Running the model through Python

Now that you have a model file, let's write a simple Python script to load it, run the engine, and see the results. The SESL engine exposes two main functions that you will use frequently.

The first function is load_model_from_yaml(text). This function takes the text content of a model file and parses it into compiled rules and example scenarios. Think of this as translating the human-readable model into a form the engine can execute.

The second function is forward_chain(rules, facts, monitor, ...). This is the actual inference engine. You pass in the compiled rules, the facts (input data), and optionally a monitor object for explainability. The engine evaluates the rules and returns the updated facts with results and explanations.

Here is a simple runner script that demonstrates how to use these functions. Create a new Python file called run_example.py in the same directory as your model file, and paste this code:

import pathlib
from sesl_engine import load_model_from_yaml, forward_chain, Monitor

# Load model text from the file we created
model_path = pathlib.Path("loan-example.sesl.yaml")
text = model_path.read_text(encoding="utf-8")

# Parse the model into rules and named scenarios
rules, scenarios = load_model_from_yaml(text)

# Take the first scenario to test with
scenario_name, facts = scenarios[0]

# Create a monitor object to capture explanations
monitor = Monitor(theme="colour")

# Run the inference engine
forward_chain(rules, facts, monitor=monitor)

# Print out the results
print("Scenario:", scenario_name)
print("Result:", facts.get("result"))

4.5 Understanding the output

When you run the script above, the engine produces several important pieces of information. Understanding what each piece means will help you trust the results and debug any issues that arise.

Here is a sample output that you might see when running the loan approval example:

Scenario: Applicant A
Result: {
  'high_income': True,
  'decision': 'approve',
  'decision_reason': 'Very high income',
  'monitor': [...],
  'monitor_blocks': [...],
  'monitor_theme': 'colour'
}

The Result Field: This is what your application will actually use. It contains the output of your rules. In our loan example, the result field tells you whether the loan was approved or denied. Each key in the result object represents a fact that was computed during rule evaluation. In this case, high_income was set to True by the first rule, and decision was set to 'approve' by the second rule. The result is deterministic, meaning the same input facts will always produce the same result.

The Decision Reason: Notice the decision_reason field. This is an example of a derived fact that the rules created. Rules don't just compute decisions; they can also compute explanations. This makes your application more useful because you can show users not just what was decided, but why.

The Monitor Object: If you created a monitor object like in the example above, the result includes monitor and monitor_blocks fields. These contain rich explainability information about how the decision was made. The monitor tracks every rule that fired, every condition that was evaluated, and every fact that was derived. This is invaluable for explaining decisions to users, auditors, or when debugging unexpected results.

To understand why a decision was made, you can print the monitor object directly. It contains detailed information about rule execution order, condition evaluation results, and the full dependency chain that led to your final result. This allows you to answer questions like "Why was the loan denied?" with complete accuracy and transparency, showing exactly which rules fired, which conditions were true, and in what order the evaluation happened.

5. Structure of a SESL model

Every SESL model is a structured document that follows a consistent format. This structure makes models easy to read, maintain, and understand. At the top level, a SESL model file contains five main sections, each serving a specific purpose.

The model section provides the name or identifier of your model. This is a human-readable label that tells anyone reading the file what the model is for. For example, you might have a model named "Loan Approval Engine" or "Fraud Detection Rules".

The meta section is optional but highly recommended. This is where you store metadata about your model such as the author name, version number, creation date, last modification date, source document reference, and any important usage notes or caveats. Metadata helps teams understand the context and history of a model, making it easier to maintain and update over time. Common metadata fields include: author (who created or owns the model), version (semantic version number for tracking changes), created (creation date), modified (last update date), source (reference to originating document or policy), and considerations (important notes or warnings about usage).

The const section defines constants that can be referenced throughout your rules. Constants are values that do not change within a single execution of the engine. They are a powerful way to avoid hard-coding numbers and thresholds directly into your rules. For example, you might define a "minimum_credit_score" constant that multiple rules reference.

The rules section is where the decision logic lives. This section contains a list of rules that the engine will evaluate. Each rule specifies conditions that must be met and actions that should be taken when those conditions are true. Rules are the heart of your SESL model, and we will explore them in detail in the next section.

The facts section provides example input scenarios for testing and demonstration. These are not just documentation; they are actual test cases that you can run against your model to verify it works correctly. Each fact scenario describes a specific situation and the expected result.

5.1 Example model with annotations

# Model name
model: "Customer loyalty scoring"

# Optional descriptive metadata
meta:
  author: "Loyalty team"
  version: "1.2.0"
  created: "2025-02-10"
  modified: "2025-12-08"
  source: "Marketing rules document v3"
  considerations: "Not for credit risk decisions"

# Constants shared by all rules
const:
  gold_threshold: 1000
  silver_threshold: 500

# List of rule objects
rules:
  - rule: "Set gold tier"
    priority: 20
    if: "customer.points >= const.gold_threshold"
    then:
      result.tier: "gold"
    reason: "Customer points are greater than or equal to gold threshold"

  - rule: "Set silver tier"
    priority: 10
    if: "customer.points >= const.silver_threshold"
    then:
      result.tier: "silver"
    reason: "Customer points are greater than or equal to silver threshold"

# Example fact scenarios
facts:
  - name: "Customer with 1200 points"
    customer:
      points: 1200
    result: {}

  - name: "Customer with 700 points"
    customer:
      points: 700
    result: {}

This example shows how all parts work together. Notice how the rules reference the constants defined in the const section using the const. prefix. This allows you to update threshold values in one place rather than searching through all your rules.

Each fact scenario contains an initial result mapping, which starts as empty. As the engine evaluates rules, it will populate this result mapping with the decisions and computed values. The facts section acts as your test suite, allowing you to run the same rules against different input scenarios to verify behavior.

Notice the priority values on the rules. The "Set gold tier" rule has priority 20, while "Set silver tier" has priority 10. Higher priority rules are evaluated first. This ensures that the most specific rules (like checking for gold tier) run before more general rules (like checking for silver tier), preventing unexpected behavior where a customer might match the silver rule before the gold rule is evaluated.

6. Rules structure

Rules are the core of every SESL model. A rule in SESL is a simple but powerful structure that connects conditions with actions. The idea is straightforward: if certain conditions are satisfied, then execute certain actions. Actions typically write values into the facts structure, allowing rules to communicate results and intermediate values to other rules.

6.1 Anatomy of a rule

Every rule in SESL follows a consistent structure. Here is an example of a complete rule with all possible fields:

- rule: "Human readable rule name"
  priority: 10
  if: "customer.age >= 18"
  let:
    age_band: "customer.age >= 65"
  then:
    result.is_adult: true
  reason: "Customer has reached legal adulthood"
  stop: false

The rule field is required and provides a human-readable name for the rule. This name appears in explanations and monitoring output, so choose a name that clearly describes what the rule does. For example, "Check if customer is eligible" is much better than "Rule 1".

The priority field is optional but very useful for controlling execution order. It is an integer where higher values indicate higher priority. The engine evaluates high-priority rules before low-priority rules. This is essential when you have rules that depend on each other or when you want more specific rules to run before more general ones.

The if field specifies the condition or nested condition structure that must be satisfied for the rule to fire. If the condition evaluates to true, the rule executes. If it evaluates to false, the rule does not execute and its actions are not taken.

The let field is optional and defines helper variables that are computed before evaluating the rule's actions. This is useful when you have complex expressions that you want to compute once and then use in multiple places. For example, you might compute "is_senior_citizen" once in the let section and then use it in the then section.

The then field specifies the mapping of target paths to expressions that should be written when the rule fires. In the example above, when the condition is true, the engine writes the value "true" to the path "result.is_adult". The then field is how rules produce output values.

The reason field is optional but highly recommended. It provides a human-readable explanation of why the rule exists. This explanation is included in the monitor output, helping anyone who reviews the results understand the business logic behind the decision.

The stop field is optional and defaults to false. When you set stop to true, the engine stops evaluating any remaining rules after this rule fires. This is useful for rules that represent final decisions from which you do not want other rules to deviate.

6.2 Rule evaluation order

The order in which the SESL engine evaluates rules is deterministic and intelligent. Understanding this order helps you write rules that work correctly together.

First, the engine groups rules by priority. All rules with the highest priority number are evaluated first. Then rules with the next highest priority are evaluated, and so on. This is how you control which rules run before others.

Within each priority group, the engine performs data-driven dependency ordering. If one rule writes to a fact that another rule reads, the writing rule is evaluated before the reading rule. For example, if Rule A writes to "result.approved" and Rule B reads from "result.approved", then Rule A will be evaluated before Rule B, regardless of their positions in the file.

Within a single priority group when there are no dependencies between rules, the engine preserves the original order from the model file. This means that if two rules have the same priority and neither reads or writes to values the other uses, they will execute in the order they appear in your model file.

The engine runs in multiple iterations. It evaluates all rules once, checks if any facts changed, and if they did, it runs all the rules again. This forward chaining behavior continues until either no facts change in an iteration or until the maximum number of iterations is reached. This allows rules to build on values produced by other rules, creating sophisticated decision logic through simple, composable rules.

7. Rule statements and full syntax

SESL supports several kinds of statements that you can use inside rules to build sophisticated decision logic. This section describes each statement type with syntax, examples, and guidance on when to use each one.

7.1 Condition expressions

The simplest form of a condition is a single expression string in the if field. A condition expression compares two values using an operator and returns true or false. When the condition is true, the rule fires and its actions execute.

The basic form of a condition expression is:

<left operand> <operator> <right operand>

The left operand is typically a path into your facts, like customer.age or application.income. The operator specifies what kind of comparison to perform. The right operand can be a literal value (like 'United Kingdom' or 18), another path, or even a constant reference.

SESL supports the following comparison operators:

  • == checks if two values are equal
  • != checks if two values are not equal
  • > checks if the left value is greater than the right value
  • >= checks if the left value is greater than or equal to the right value
  • < checks if the left value is less than the right value
  • <= checks if the left value is less than or equal to the right value
  • in checks if the left value is a member of the right container (like a list)
  • not in checks if the left value is not a member of the right container

Here are some practical examples of condition expressions. You might write "customer.country == 'United Kingdom'" to check if the customer is in a specific country. Or "applicant.income >= 50000" to check if income meets a minimum threshold. Or "applicant.risk_level in ['low', 'medium']" to check if the risk level is in an acceptable set.

7.2 Logical condition blocks

For more complex decision logic, SESL allows you to combine multiple conditions using logical operators. Instead of putting a simple expression string in the if field, you can use a structured mapping with logical keys like all, any, and not.

The all key means all conditions must be true for the rule to fire. The any key means at least one condition must be true. The not key negates a condition, making it true when the inner condition is false. These can be nested to create arbitrarily complex logic.

Here is an example that shows how to combine conditions with logical operators:

if:
  all:
    - "customer.age >= 18"
    - any:
        - "customer.country == 'United Kingdom'"
        - "customer.country == 'Ireland'"

This condition says: "The customer must be 18 or older AND the customer must be from either the United Kingdom or Ireland." The top-level all means both the age check and the country check must be true. The nested any means the customer can satisfy the country requirement by being from either of the two countries.

You can nest these logical blocks as deeply as needed. For example, you might have an all block containing two any blocks, or an any block containing a not block, and so on. This allows you to express complex business logic clearly and readably.

7.3 Let statements

The let section allows you to define intermediate values that you can reference in conditions and actions. Think of let variables as helper values that simplify your rules by avoiding repetition and making logic clearer.

Each entry in the let mapping is a variable name paired with an expression. The engine evaluates each expression and stores the result. You can then reference these variables anywhere else in the rule. This is particularly useful when you have a complex expression that you need to use in multiple places.

Here is an example:

let:
  income_band: "applicant.income >= 80000"
  total_exposure: "existing_loans + requested_amount"

In this example, income_band will be true if the applicant earns 80,000 or more, and total_exposure will be the sum of existing loans and the requested amount. You can then reference these variables in your conditions or actions. For instance, you might write a condition like "let.income_band == true" or use total_exposure in a calculation.

Let variables are computed once when the rule is evaluated and are not persisted in the facts structure unless you explicitly write them to the facts in the then section. This makes them useful for temporary calculations within a single rule.

7.4 Actions in the then block

The then block specifies what happens when a rule fires. It is a mapping from target paths to expressions. When the rule's conditions are satisfied, the engine evaluates each expression and writes the result to the corresponding path in the facts.

Here is an example:

then:
  result.decision: "approve"
  result.score: "base_score + bonus"

In this example, when the rule fires, the engine writes the string "approve" to result.decision and evaluates the expression base_score + bonus, writing the result to result.score. Paths are dot-separated sequences like result.decision or customer.flags.high_risk.

Paths allow you to organize your output into nested structures. For example, you might have a result object with fields for decision, score, explanation, and confidence. The expressions on the right-hand side can reference facts, constants from the model, and helper variables from the let block.

One important detail: in strict path mode, intermediate parts of a path must exist. For example, to write to result.flags.high_risk, the result and result.flags objects must already exist. However, you can enable automatic path creation, which causes the engine to create any missing intermediate structures automatically. This is controlled by the SESL_AUTO_CREATE_PATHS configuration option, which is enabled by default.

7.5 Summary table of rule statement fields

Field Purpose Key elements Notes
rule Human readable name of the rule. Text value. Required. Used in monitor output and explanations.
priority Controls evaluation order and conflict resolution. Integer, higher means earlier evaluation. Optional. Defaults to zero when omitted.
if Specifies conditions for rule firing. May be a single expression string or a structured logical mapping. May be omitted. A rule without conditions always matches.
let Defines helper values for use in conditions and actions. Mapping from variable names to expressions. Variables cannot reference themselves directly.
then Describes which paths to write when the rule fires. Mapping from target paths to expressions. Required for rules that are expected to change facts.
reason Explanation text used in monitoring and justification. Text value. Strongly recommended for explainability.
stop Indicates whether engine evaluation should stop after this rule fires. Boolean value. Use sparingly when early stopping is clearly required.

8. Expressions and functions

SESL supports a range of operators and functions that can be used in conditions, helper expressions (let blocks), and action assignments. This section covers the syntax and behavior of these operators and built-in functions.

8.1 Operand types and type system

SESL supports a dynamic type system, meaning variables can hold different types of values and the engine is intelligent about converting between types when appropriate. However, you can enforce strict type checking if you want additional safety. The primary types are:

Boolean: True or false values, used in conditions and logical operations. Booleans are the result of comparisons and logical operations.

Number: Integer or floating-point values used in comparisons and arithmetic. SESL automatically handles conversion between integers and floats for operations like division.

String: Text values enclosed in single or double quotes. Strings support concatenation and can be compared using comparison operators.

List: Ordered collections of values, typically used with the in and not in operators to test membership. Lists are also returned by certain functions.

Mapping: Key-value structures that form the nested fact hierarchy. Most values in your facts are mappings (dictionaries/objects).

None/null: The absence of a value, used to test whether fields are unset. You can check for null values using the is None operator.

When SESL_STRICT_OPERANDS is enabled in configuration, the engine enforces type compatibility in expressions and reports errors when operations don't make sense (like adding a number and a string). In relaxed mode (the default), automatic type coercion is applied where sensible, for example converting "123" (string) to 123 (number) if used in arithmetic.

8.2 Comparison operators

Comparison operators test relationships between values and return either true or false. These operators are fundamental to writing conditions in your rules.

== tests whether two values are equal. It works for all types. For example, "customer.status == 'active'" returns true if the customer status is exactly 'active'.

!= tests whether two values are not equal. For example, "result.decision != 'pending'" returns true if the decision is anything other than 'pending'.

> and >= test whether the left value is greater than (or greater than or equal to) the right value. These work with numbers and also with strings (using alphabetical order). For example, "applicant.income > 30000" returns true if income exceeds 30,000.

< and <= test whether the left value is less than (or less than or equal to) the right value.

in tests whether the left value is a member of the right value (which should be a list or container). For example, "customer.country in const.approved_countries" returns true if the customer's country appears in your approved countries list.

not in is the opposite of in. For example, "applicant.state not in const.restricted_states" returns true if the state is not in your restricted states list.

is None tests whether a value is null or missing. For example, "result.flags is None" returns true if the flags field does not exist or is explicitly set to null.

is not None is the opposite of is None. It returns true when a value exists and is not null. This is useful for checking whether required fields have been set.

8.3 Logical operators and boolean logic

Boolean operators combine multiple conditions into more complex logic. They work with values that are true or false, and they return true or false as their result.

and requires all conditions to be true. For example, "applicant.age >= 21 and applicant.income > 30000" is true only when both conditions are true: the applicant is at least 21 years old AND earns more than 30,000.

or requires at least one condition to be true. For example, "customer.is_vip or customer.lifetime_value > 100000" is true if the customer is VIP OR has lifetime value exceeding 100,000 (or both).

not inverts the truth value. For example, "not customer.is_flagged" is true when the customer is not flagged.

For very complex boolean logic with many nested conditions, SESL recommends using the structured all, any, and not blocks in the if section of rules (see section 7.2). These are clearer and easier to maintain than deeply nested inline boolean expressions.

8.4 Arithmetic operators

Arithmetic operators perform mathematical calculations on numeric values. When you use these operators, SESL automatically ensures both operands are numbers, either through coercion or (in strict mode) by raising an error if they are not.

+ performs addition. For example, "base_score + bonus" adds two numbers. The + operator also works with strings for concatenation (combining them together).

- performs subtraction. For example, "total_price - discount_amount" subtracts one number from another.

* performs multiplication. For example, "quantity * unit_price" multiplies two numbers.

/ performs division and always returns a floating-point result. For example, "total_debt / income" computes the debt-to-income ratio. Note that dividing by zero will raise an error.

// performs integer division, rounding down to the nearest whole number. For example, "total_amount // batch_size" tells you how many complete batches fit into a total.

% returns the remainder (modulo) after division. For example, "transaction_count % 10" returns the remainder after dividing by 10.

** raises a number to a power (exponentiation). For example, "interest_rate ** years" computes compound growth. Note that raising 0 to a negative power will raise an error.

8.5 Operator precedence and grouping

When an expression contains multiple operators, SESL evaluates them in a specific order, following standard mathematical conventions. Understanding precedence is important to avoid unexpected results.

SESL follows this precedence, from highest priority (evaluated first) to lowest priority (evaluated last):

  1. Parentheses: (...) – Always evaluated first, allowing you to override normal precedence
  2. Exponentiation: ** – Raises a number to a power
  3. Unary negation: -x, not x – Applied to a single value
  4. Multiplication, division, modulo: *, /, //, % – Evaluated left to right
  5. Addition, subtraction: +, - – Evaluated left to right
  6. Comparisons: ==, !=, >, >=, <, <=, in, not in, is, is not – Evaluated left to right
  7. Logical AND: and – All conditions must be true
  8. Logical OR: or – At least one condition must be true

Use parentheses to make complex expressions explicit and to ensure your intent is clear. For example, without parentheses "a + b * c" is different from "(a + b) * c" because multiplication has higher precedence. Using parentheses makes your model easier to understand and less prone to mistakes.

8.6 Built-in functions

SESL provides a set of built-in functions that handle common operations. These functions can be used anywhere an expression can be used: in conditions, in let blocks, and in then actions. Using functions reduces complexity and makes your rules more readable.

max(a, b) returns the greater of two values. For example, "max(applicant.income, minimum_income)" ensures you use at least the minimum income in calculations, even if the applicant's actual income is lower.

min(a, b) returns the lesser of two values. For example, "min(transaction.amount, limit)" ensures the transaction does not exceed your maximum limit.

abs(x) returns the absolute value (magnitude without sign). For example, "abs(variance)" converts a variance that might be negative to its positive magnitude.

len(x) returns the length of a list or string. For example, "len(customer.addresses)" tells you how many addresses a customer has, or "len(applicant.name)" tells you the number of characters in a name.

sum(list) sums all numbers in a list. For example, "sum(transaction.amounts)" computes the total of all transaction amounts.

avg(list) and mean(list) compute the average (mean) of a list of numbers. For example, "avg(monthly_expenses)" tells you the average expense per month.

round(x, n) rounds a number to n decimal places. For example, "round(calculated_rate, 2)" rounds to 2 decimal places, suitable for percentages.

floor(x) rounds down to the nearest integer. For example, "floor(score)" converts 85.7 to 85.

ceil(x) rounds up to the nearest integer. For example, "ceil(cost)" converts 99.1 to 100, useful when you want to round up any partial amount to the next whole unit.

str(x) converts a value to a string. For example, "str(applicant.id)" converts an ID number to text.

int(x) converts a value to an integer. For example, "int(score_string)" converts the string "85" to the number 85.

bool(x) converts a value to a boolean (true or false). For example, "bool(flag_value)" ensures the result is a proper boolean.

8.7 String operations

SESL supports basic string operations for working with text values. Strings are enclosed in single or double quotes and can be compared, combined, and searched.

Concatenation: Use the + operator to combine strings. For example, "'Hello, ' + customer.name" combines the greeting with the customer's name. You can concatenate multiple strings and even mix strings with numbers (the numbers are automatically converted to strings).

String comparison: Use comparison operators like == and != to check string values. For example, "applicant.status == 'active'" checks whether the status is exactly 'active'. String comparisons are case-sensitive by default.

Membership: Check if a substring exists within a string using the in operator. For example, "'restricted' in document_text" returns true if the word 'restricted' appears anywhere in the document text.

Case methods: The methods .lower() and .upper() convert strings to lowercase or uppercase respectively. For example, "customer.name.lower()" converts a customer name to all lowercase, useful for case-insensitive comparisons.

8.8 Expression context and scope

Expressions can reference different sources of data depending on where they appear in your rules. Understanding what is available in each context helps you write correct and clear expressions.

In condition expressions (the if field), you can reference facts via dot notation (like customer.age), constants via the const. prefix (like const.minimum_age), and helper variables from the enclosing let block.

In let blocks, you can reference facts, constants, and previously defined helper variables. However, a variable cannot reference itself (self-referential expressions are a compile-time error). Variables are evaluated in order, so earlier variables are available to later ones.

In then actions, you can reference facts, constants, helper variables, and the special result namespace. You can also reference previously written result values within the same then block, allowing you to build up complex output values step by step.

8.9 Expression examples

Here are some realistic expression examples to help you understand how to use expressions in practice:

# Simple arithmetic
then:
  result.total: "item_price * quantity"

# Conditional using max
let:
  effective_income: "max(applicant.income, applicant.spouse_income)"

# Boolean combination
if: "applicant.age >= 18 and applicant.credit_score > 620"

# String concatenation
then:
  result.message: "'Risk level: ' + result.risk_band"

# Complex arithmetic with functions
let:
  debt_ratio: "total_debt / max(applicant.income, 1)"
if: "debt_ratio > 0.5 or applicant.income < min_income"

# List operations
if: "applicant.country in const.approved_countries"
then:
  result.discount_rate: "0.15"

9. Facts structure

Facts represent the input data that rules operate on. They are the raw information that your application gathers from users, databases, or external services. Facts are structured as nested mappings (dictionaries or objects) of keys to values. The SESL engine treats the top level of the facts mapping as the main namespace where rules read and write values using dot-separated paths like customer.age or result.decision.

9.1 Facts inside a model file

Within a SESL model file, the facts section is a list of test scenarios. Each scenario has its own copy of the facts mapping, allowing you to test your rules against different input combinations. When you run the model file through the engine, it evaluates the rules once for each scenario.

Here is a typical facts structure:

facts:
  - name: "Young customer"
    customer:
      age: 22
      country: "United Kingdom"
    result: {}

  - name: "Senior customer"
    customer:
      age: 70
      country: "United Kingdom"
    result: {}

Notice how the first scenario is named "Young customer" and contains customer information (age and country). The second scenario is "Senior customer" with different age but same country. Each scenario starts with an empty result mapping, which the rules will populate when they execute.

A key pattern in SESL is to use nested keys to group related information. In the example above, all customer data is grouped under the customer key. This keeps your facts organized and makes rules easier to read and maintain. You might have other top-level keys like application, credit_report, transaction, and so on.

Including an initial result mapping (even if empty) makes your intent clear. Although the engine will automatically create a result mapping if it is missing, explicitly including it documents that you expect rules to write output into this section. This convention improves code clarity.

9.2 Engine metadata in facts

When the SESL engine runs, it stores its own internal metadata under a reserved top level key named _sesl. This namespace is entirely managed by the engine and you should not access it from your rules.

The _sesl namespace contains several types of information. First, it holds a snapshot of the original facts as they were when the engine started, which is useful for comparison and rollback scenarios. Second, it stores support information for truth maintenance, tracking which rules produced which facts so that when a fact becomes invalid, the engine can determine which other facts depend on it. Third, it includes monitoring information about which rules fired, how conditions were evaluated, and execution metrics. Fourth, it contains a copy of the configuration settings used for the current run.

Since _sesl is reserved for engine data, you should never write facts under this key in your rules. If you do, the engine will either ignore them or raise an error, depending on configuration. Avoid using keys that start with underscore in your own facts to prevent accidental conflicts.

9.3 How rules consume facts

Rules access facts using dot-separated path notation. A path describes a route through your nested facts structure. For example, customer.age means "look at the customer object and get the age field." Similarly, result.score means "look at the result object and get the score field."

When the engine evaluates a rule, it resolves these paths by traversing the facts mapping. If the path exists, the engine retrieves the value. If the path does not exist, the engine returns a null or missing value, depending on the configuration mode.

In strict path mode (controlled by the SESL_STRICT_PATHS configuration option), the engine raises an error when it encounters a missing path. This protects you from subtle mistakes such as misspelling a key. For example, if you write customer.age but the actual key is customer.aget, strict mode will catch this typo and alert you. In relaxed mode, the engine returns a null value for missing paths, allowing rules to handle missing data gracefully.

10. Engine configuration

The SESL engine behavior can be customized through configuration settings. These settings control how strictly the engine checks types and paths, how it handles missing values, and how it reports errors. Different applications have different requirements—a financial application might need strict type checking, while a customer service application might prefer lenient handling of missing data. Configuration allows you to tune the engine's behavior to match your needs.

Configuration can be set in three different ways: via environment variables that apply globally to the entire system, via configuration objects passed directly to the engine function, or by relying on sensible defaults. We will explore each approach.

10.1 Environment variables

Environment variables provide a convenient way to configure the engine globally. These settings are checked when the engine initializes and apply to all rules in all models run in that environment. This is useful for setting global policies across your entire application.

SESL_STRICT_OPERANDS (true or false) controls whether the engine enforces type compatibility in expressions. When set to true, the engine will refuse operations that mix types without explicit conversion. For example, it will not add a string to a number; you must explicitly convert one or both to a compatible type. This prevents subtle bugs where a "123" string might unexpectedly behave like the number 123. Default: false (relaxed mode allows coercion).

SESL_STRICT_PATHS (true or false) controls whether the engine requires all intermediate structures to exist before you write to a path. When true, writing to result.flags.high_risk requires that both result and result.flags already exist as objects. This prevents typos where you might accidentally create result.flgas instead of result.flags. Default: false (relaxed mode creates missing structures).

SESL_AUTO_CREATE_PATHS (true or false) controls whether the engine automatically creates missing intermediate structures when writing to paths. When true, writing to result.nested.deep.field will automatically create any missing intermediate objects. This is convenient for rapid development but can hide typos. Default: true (automatic creation enabled). Note that this setting is ignored if SESL_STRICT_PATHS is true.

SESL_ERROR_STYLE (fancy or plain) controls whether error messages include formatting and colors. Fancy mode produces colorful, visually formatted error messages useful for development. Plain mode produces simple text suitable for logging systems that don't support colors. Default: fancy.

SESL_CONFLICT_POLICY (error, warn, or ignore) controls how the engine handles conflicting writes. A conflict occurs when multiple rules at the same priority level try to write different values to the same path in the same iteration. The error policy raises an exception, the warn policy logs a warning and picks one value, and the ignore policy silently picks one value. Default: warn.

SESL_MAX_ITERATIONS (integer) sets the maximum number of forward chaining iterations. This prevents infinite loops if your rules form a cycle that continuously updates facts. When the limit is reached, the engine stops and returns the current state. Default: 20.

10.2 Configuration via function arguments

Configuration can also be passed directly to the forward_chain function via a configuration object. This approach allows you to override environment variables and defaults for specific runs without modifying system settings.

from sesl_engine import forward_chain, EngineConfig

config = EngineConfig(
    strict_operands=True,
    strict_paths=False,
    auto_create_paths=True,
    error_style="fancy",
    conflict_policy="warn",
    max_iterations=20
)

forward_chain(rules, facts, config=config, monitor=monitor)

This pattern is useful when you want different configurations for different parts of your application. For example, your high-stakes approval engine might use strict checking, while your analytics engine might use relaxed checking for flexibility.

10.3 Configuration precedence

When the engine needs to know a configuration value, it checks multiple sources and uses the highest-priority source available. Understanding this precedence helps you set configuration predictably across your application.

The precedence order from highest to lowest is:

  1. Function arguments: Configuration passed directly to forward_chain via the config= parameter has the highest priority
  2. Environment variables: Settings like SESL_STRICT_OPERANDS are checked second
  3. Engine defaults: Built-in defaults are used if no function arguments or environment variables are set

This design allows you to set global defaults via environment variables while overriding them for specific runs. For instance, you might set SESL_STRICT_OPERANDS=false globally for development, but pass config=EngineConfig(strict_operands=True) when running your production approval engine.

10.4 Strict mode recommendations

Different applications have different safety requirements. We recommend enabling strict modes in production to catch subtle bugs early. The right configuration depends on your use case and risk tolerance.

For financial calculations, loan approvals, and other high-stakes decisions where correctness is critical, we strongly recommend setting strict_operands=True. This forces explicit type conversions and catches type mismatches, preventing unexpected behavior. Without strict mode, a string "100" might accidentally be treated as the number 100, leading to wrong decisions.

For models that process structured input data, set strict_paths=True with auto_create_paths=False. This ensures all paths are explicitly defined in your facts before rules write to them. If a rule tries to write to a path that doesn't exist, the engine raises an error immediately rather than silently creating it. This catches typos and missing initialization issues early.

For production models where multiple rules might write to the same path, set conflict_policy="error". This treats conflicting writes as errors rather than warnings, ensuring you are aware of any ambiguities in your rule set. This promotes deterministic behavior and prevents subtle race conditions where the order of rules might affect results.

11. Validation and linting

Before running a SESL model in production, it is important to validate that the model is well-formed and that rules do not contain obvious errors. Mistakes in rule logic can have serious consequences—approving ineligible applicants, denying eligible ones, or applying incorrect discounts. SESL provides several validation and linting tools to help you identify and fix issues early in your development process, before they reach production.

11.1 Preflight validation

Preflight validation is automatic and happens immediately when you load a model file using load_model_from_yaml. This first pass checks the basic structure and syntax of your model before the engine tries to execute it.

Preflight validation checks several important things. First, it verifies that the YAML syntax is correct. YAML is sensitive to indentation and special characters, so syntax errors are common. Second, it confirms that all required fields are present in each rule, such as the rule name and the then section. Third, it checks that all rule names are unique within the model—if you accidentally define two rules with the same name, the engine will catch this. Fourth, it scans for obviously missing references, such as constants that are used in rules but never defined in the const section.

If preflight validation fails, a clear and descriptive error message is raised immediately, telling you exactly what went wrong and where to find it in your model file. This means you can fix problems quickly without waiting to run the full engine.

11.2 Model linting

In addition to basic validation, SESL provides a linter that performs much deeper semantic analysis. The linter looks beyond simple syntax and structure to understand the logic of your rules and find potential problems.

Here is how to run the linter on your model:

from sesl_engine.tools.linter_core import lint_model_from_yaml

model_text = open("my_model.sesl.yaml").read()
lints, errors = lint_model_from_yaml(model_text)

for lint in lints:
    print(f"Warning: {lint.message} at rule '{lint.rule_name}'")

for error in errors:
    print(f"Error: {error.message}")

The linter returns two lists: errors (which indicate serious problems that should be fixed) and lints (which are warnings about potential issues that you might want to address). You can use this information to improve your model before it reaches production.

The linter checks for several categories of problems. First, unused variables—helper variables defined in let blocks that are never referenced anywhere. These indicate incomplete logic or leftover code from refactoring. Second, unreachable code—rules that can never fire because their conditions are logically impossible or contradictory. Third, suspicious patterns—rules that write to facts without reading any input (suggesting they might be incorrect), or complex conditions that are hard to understand and therefore hard to maintain. Fourth, type mismatches—operations that combine incompatible types (when strict mode is enabled). Fifth, missing documentation—fields that are written by rules but never have an explanation or reason provided.

11.3 Dependency analysis

The SESL engine can analyze the dependencies between your rules, showing which rules depend on values produced by other rules. This dependency analysis helps you understand rule ordering and identify potential conflicts or circular dependencies.

Here is how to use the dependency analysis tool:

from sesl_engine import build_dependency_graph

graph = build_dependency_graph(rules)

# Each rule shows which other rules it depends on
for rule in rules:
    dependencies = graph.get_dependencies(rule.name)
    print(f"{rule.name} depends on: {dependencies}")

Understanding dependencies is useful for several reasons. First, it helps you reorder rules to improve clarity—even though the engine automatically reorders rules based on dependencies, explicitly arranging them in your model file makes the logic easier for humans to follow. Second, it helps you identify potential circular dependencies where Rule A depends on Rule B and Rule B depends on Rule A. The engine prevents circular dependencies from causing infinite loops, but they indicate design issues that should be addressed. Third, dependency analysis helps when refactoring large models into smaller, more focused sub-models—you can group rules that depend on each other into separate files.

11.4 Testing and scenario validation

The facts section of a SESL model serves dual purposes: it provides test data for learning and experimenting, and it acts as a regression test suite to catch unexpected changes in rule behavior. Each fact scenario can be run independently to verify that rules behave as expected.

Here is a common pattern for testing your rules against all scenarios:

rules, scenarios = load_model_from_yaml(model_text)

for scenario_name, facts in scenarios:
    monitor = Monitor()
    forward_chain(rules, facts.copy(), monitor=monitor)
    
    # Verify expected outcomes
    assert facts["result"]["decision"] in ["approve", "decline"], f"Scenario {scenario_name} produced unexpected result"
    print(f"✓ Scenario {scenario_name} passed")

This approach allows you to maintain a suite of test cases alongside your rules. When you modify your rules, you can run this test suite to ensure that changes do not break expected behavior. For example, if you change the income threshold in one rule, your tests will immediately tell you if this change affected the outcomes of your test scenarios in unexpected ways.

Best practice is to include test scenarios that cover the important decision boundaries in your model. For instance, if your model determines loan eligibility based on credit score, include scenarios just above and just below the eligibility threshold. This way, if someone accidentally changes the threshold value, your tests will catch it immediately.

12. How the SESL engine works

The SESL engine is a forward chaining inference engine implemented in Python. It is designed to be transparent and predictable, so you understand exactly how it arrives at decisions. This section explains the internal workings of the engine, which helps you write better rules and debug unexpected results.

12.1 High level architecture

The SESL engine follows a multi-stage pipeline to process rules and facts. Understanding these stages helps you know what happens at each step and when errors might be detected.

Stage 1: Model Loading begins when you call the load_model_from_yaml function. This function reads your YAML model file as text and parses it into internal structures. It extracts the rules, constants, and fact scenarios. This stage outputs compiled rules ready for execution and organized fact scenarios ready for testing.

Stage 2: Preflight Validation happens automatically as part of model loading. The preflight_validate_model function performs inexpensive checks such as verifying that all rule names are unique, that YAML syntax is correct, and that obviously missing constants or paths are caught. This stage is fast and runs every time you load a model.

Stage 3: Dependency Ordering analyzes relationships between rules. Rules are sorted by priority (higher priority first) and then by data dependencies (rules that produce data come before rules that consume it). This intelligent reordering ensures rules execute in an order that respects their dependencies.

Stage 4: Forward Chaining is when the actual inference happens. The forward_chain function repeatedly evaluates rule conditions and applies actions until no more facts change or the iteration limit is reached. This is the "thinking" part of the engine where decisions are made.

Stage 5: Explainability and Metrics happens throughout execution. The optional Monitor class records detailed information about which rules fired, which conditions were evaluated, and what changed. This information is essential for understanding and explaining decisions to users and auditors.

12.2 Lifecycle of a single run

When you call forward_chain, the engine goes through a well-defined lifecycle. Understanding this lifecycle helps you understand exactly what happens to your facts at each step.

First, the engine receives the compiled rules and your facts mapping. It also optionally receives engine configuration settings and a monitor instance for recording details. The engine immediately makes a snapshot of your starting facts, storing them for later use in truth maintenance and explanations. This snapshot allows the engine to answer questions like "Which facts changed during execution?" and "Why did this fact change?"

Second, the engine attaches internal metadata under the reserved _sesl key of your facts. This metadata includes the snapshot, configuration settings, rule information, and monitoring data. You should never write to the _sesl key from your rules.

Third, the engine enters a main loop that runs for at most a fixed number of iterations (typically twenty). Each iteration is a complete pass through all rules. Here is what happens in each iteration:

  1. The engine evaluates each rule in sorted order.
  2. For each rule, it prepares a _let (helper variables) mapping and computes each helper expression.
  3. It evaluates the rule conditions using the current facts and helper values.
  4. If the conditions are satisfied, the engine applies actions by writing to target paths in the facts mapping.
  5. It records monitoring events, including what rule executed, which conditions were true, and what changed.

Fourth, at the end of each iteration, the engine compares a fingerprint (hash) of the user-visible facts. If the fingerprint has not changed since the previous iteration, the engine knows the facts have stabilized and it stops. The loop doesn't run the maximum iterations; it stops as soon as facts stop changing. This is efficient and means your rules can converge in a few iterations rather than always running to the maximum.

Fifth, if all iterations complete and facts are still changing, the engine stops anyway. This prevents infinite loops from rules that form cycles. It will warn you that the maximum iterations were reached, which usually indicates that your rules might have a design issue.

12.3 Walkthrough example

Let's trace through a simple example to see how the engine works step by step. Consider this very small model:

model: "Simple flag demo"

rules:
  - rule: "Flag high amount"
    if: "transaction.amount > 1000"
    then:
      result.flagged: true
      result.flag_reason: "High transaction amount"
    reason: "Highlight unusually large transactions"

facts:
  - name: "Transaction demo"
    transaction:
      amount: 1500
    result: {}

Here is how the engine processes this model:

  1. The engine loads one rule and one scenario with transaction amount 1500.
  2. It starts the first iteration and evaluates the rule.
  3. It evaluates the condition "transaction.amount > 1000". Since the amount is 1500, this is true.
  4. Because the condition is true, the engine executes the then block, writing two values: result.flagged = true and result.flag_reason = "High transaction amount".
  5. The monitor records that the rule fired and why.
  6. At the end of the iteration, the engine compares fingerprints. The facts have changed (two new fields were added), so it checks another iteration.
  7. In the next iteration, it evaluates the same rule again. The condition is still true, but the then block would write the same values.
  8. No new facts changed this iteration, so the fingerprint matches the previous one. The engine stops.

The final result would look like this:

result: {
  "flagged": true,
  "flag_reason": "High transaction amount",
  "monitor": [...],
  "monitor_blocks": [...],
  "monitor_theme": "colour"
}

In this simple example, the engine only needed two iterations to reach a stable state. More complex models with many interdependent rules might need more iterations, but the principle is the same: the engine keeps going until nothing changes or the iteration limit is reached.

13. Output and explanation

When you call the forward_chain function, the engine mutates the facts mapping in place, filling it with results from rule execution. The engine also attaches several helpful structures that provide detailed information about what happened during execution. This section describes the main parts of the engine output.

13.1 Result section

The most important part for your application logic is the result mapping. This is where your rules typically write decisions, scores, explanations, and other output values. The result section is entirely defined by your model—you decide what fields to include and what they mean.

result: {
  "decision": "approve",
  "decision_reason": "Very high income",
  "score": 78
}

Common patterns for result sections include:

  • decision: The main outcome—approve, decline, refer, pending, or whatever options your business logic needs.
  • decision_reason: Free text explanation of why the decision was made. This is crucial for transparency.
  • score: A numeric score or rating (0-100, 0-1000, etc.) that quantifies the assessment.
  • flags: A sub-mapping with Boolean flags describing special conditions (e.g., flags.requires_manual_review, flags.high_risk).
  • confidence: A confidence level (e.g., 0-1) expressing how certain the decision is.
  • processing_time_ms: How long the evaluation took (useful for performance monitoring).

13.2 Monitor fields

When you pass a monitor instance to the engine, it records detailed information about rule execution. For convenience, this monitoring information is attached to both the _sesl key and the result mapping, making it easily accessible.

Here is an example of monitor output:

result: {
  "decision": "approve",
  "monitor": [
    {"step": 1, "message": "--- Iteration 1 ---"},
    {"step": 2, "message": "Evaluating Set high income flag", "details": {...}},
    {"step": 3, "message": "Rule Set high income flag sets result.high_income = True"},
    {"step": 4, "message": "Rule Set high income flag FIRED", "details": {...}}
  ],
  "monitor_blocks": [
    "🟢 Rule Set high income flag\n   ├─ Evaluation:\n   │     matched: True\n   │     ...",
    "🟢 Rule Recommend approval for very high income\n   ..."
  ],
  "monitor_theme": "colour"
}

The monitor output contains three fields. The monitor field is a list of raw events in chronological order. Each event has a numeric step number, a message, and optionally detailed information about what was evaluated. This raw format is machine-readable and useful for programmatic analysis.

The monitor_blocks field is a list of human-readable multi-line blocks summarizing each rule's evaluation. Each block shows which rule was evaluated, whether the conditions matched, what actions were taken, and whether the rule fired. These blocks are formatted for easy reading and often include Unicode symbols (checkmarks, arrows, etc.) to visually indicate execution flow.

The monitor_theme field is a hint string that indicates what presentation style was used. Common values are "colour" (for colorful terminal output) or "plain" (for simple text). Your user interface can use this hint to determine how to display the monitoring information to users.

13.3 Metrics and configuration

The engine attaches execution metrics and configuration details under the reserved _sesl key of the facts mapping. This information is useful for diagnostics, performance tuning, and understanding how the engine processed your model.

Here is an example:

_sesl:
  metrics:
    iterations: 2
    rules_evaluated: 4
    rules_fired: 2
    elapsed_ms: 3.712
    convergence_fingerprint: "<hash string>"
  _config:
    strict_operands: true
    strict_paths: true
    auto_create_paths: false
    error_style: "fancy"
    conflict_policy: "warn"

The metrics section provides performance and execution information. The iterations field shows how many forward chaining cycles the engine completed before reaching a stable state. The rules_evaluated field shows the total number of times the engine evaluated rule conditions (this might be higher than the number of rules × iterations because of complex conditions). The rules_fired field shows how many times rules actually executed and changed facts. The elapsed_ms field shows the total execution time in milliseconds, useful for performance monitoring. The convergence_fingerprint is a hash of the user-visible facts used internally to detect when the state has stabilized.

The _config section documents what configuration settings were used during execution. This is useful for auditing and debugging—you can verify that rules were executed with the expected configuration (strict mode on or off, etc.).

13.4 Truth maintenance support

SESL maintains detailed support information tracking which rules wrote which facts. This support information is stored under _sesl.support and _sesl.support_detail and can be used to explain exactly why a fact has a particular value. This is the foundation of the SESL engine's explainability capabilities.

Here is an example:

_sesl:
  support:
    "result.decision": ["Approve on high income"]
  support_detail:
    "result.decision":
      "Approve on high income":
        reason: "Applicant has very high income"
        priority: 10

The support structure maps from result paths to the list of rules that wrote those paths. The support_detail provides more context about each rule, including its reason and priority. You can use the helper function explain_fact(facts, "result.decision") to retrieve a structured justification chain for any path, which is helpful for building user-facing explanations and audit trails.

14. Truth maintenance and dependency tracking

One of the most powerful features of SESL is its ability to track and explain every fact produced by your rules. Truth maintenance is the practice of recording which rules wrote which facts so that you can later explain exactly why a decision was made. This section describes how truth maintenance works and how to use it to build transparent, auditable systems.

14.1 Support information structure

When rules fire and write values, the SESL engine automatically records which rule wrote each value and under what conditions. This information is called "support" or "provenance." It is stored in the _sesl.support and _sesl.support_detail sections of the facts mapping.

Here is an example:

_sesl:
  support:
    "result.decision": ["Approve on high income"]
    "result.score": ["Calculate base score", "Apply income bonus"]
  
  support_detail:
    "result.decision":
      "Approve on high income":
        reason: "Applicant has very high income"
        priority: 10
        rule_fired_at_iteration: 1
    
    "result.score":
      "Calculate base score":
        reason: "Starting score from applicant profile"
        priority: 50
      "Apply income bonus":
        reason: "Bonus applied for stable income"
        priority: 40

The support field is a mapping from fact paths to lists of rule names that wrote to those paths. This tells you exactly which rules are responsible for each output value. The support_detail field provides additional context about each write operation, including the rule's reason explanation, its priority, and which iteration it fired in.

This structure is the foundation of SESL's explainability. Every output value can be traced back to the rules that produced it, and every rule can be linked to its reason explanation. This creates a complete audit trail of how decisions were made.

14.2 Explaining a specific fact

You can use the explain_fact helper function to construct a human-readable explanation of why a fact has its current value. This function traces through the support information and compiles a structured justification.

Here is an example of using explain_fact:

from sesl_engine import explain_fact

# After running forward_chain...
explanation = explain_fact(facts, "result.decision")

print(f"Path: {explanation.path}")
print(f"Value: {explanation.value}")
print(f"Supported by rules: {explanation.supporting_rules}")
for rule_name, details in explanation.rule_details.items():
    print(f"  - {rule_name}: {details['reason']}")

This produces output like:

Path: result.decision
Value: approve
Supported by rules: ['Approve on high income']
  - Approve on high income: Applicant has very high income

You can use this in your application to show users exactly why a decision was made. For example, in a loan approval system, you could display "Your application was approved because: Applicant has very high income." This transparency builds trust and makes it easier to explain decisions to applicants, regulators, and internal stakeholders.

14.3 Multiple writes and conflict resolution

When multiple rules try to write to the same path, the support information tracks all of them, but only one value "wins" and becomes the final result. The engine determines which value wins through a priority-based conflict resolution process.

First, the engine respects rule priority. If Rule A has priority 20 and Rule B has priority 10, Rule A's value will be used. Second, if two rules have the same priority and both fire in the same iteration, the conflict policy determines what happens. The default conflict policy is "warn," which logs a warning but lets one rule win (the order is implementation-dependent). You can set it to "error" to reject conflicting writes or "ignore" to silently pick one.

The support information always records which rule's value is currently "winning" (the highest priority, most recent write). This means you can explain not just why a fact has a value, but why other rules' values were rejected in favor of higher-priority rules.

14.4 Dependency graphs and rule relationships

SESL can analyze your rules to build a dependency graph—a visual representation of which rules read values and which rules write them. This helps you understand the "flow" of data through your rule set.

Here is how to build and query a dependency graph:

from sesl_engine import build_dependency_graph

graph = build_dependency_graph(rules)

# Find all rules that depend on a given path
depending_rules = graph.get_rules_reading(path="result.score")

# Find all paths that a rule writes to
written_paths = graph.get_written_paths(rule_name="Calculate bonus")

Dependency graphs are useful for several purposes. First, understanding the flow of data helps you understand what your rules do. Second, you can identify which rules are safe to modify or remove—if you remove a rule that no other rule depends on, nothing breaks. Third, you can detect unused rules or dead code—rules that never fire or that nobody depends on. Fourth, you can refactor large models to improve maintainability by grouping related rules.

14.5 Building user interfaces with explanation data

The support and monitor information generated by SESL can be used to build rich user interfaces that explain decisions. Rather than showing users a black box that says "approved," you can show them exactly why the decision was made and which rules were involved.

Here are some UI patterns you can build with SESL explanation data:

  • Decision summary: Show the final decision and the primary rule that determined it. For example, "Your application was approved because you meet the high income threshold."
  • Rule trace: Display all rules that fired, in the order they executed, with their conditions and actions. This helps advanced users understand the logic.
  • Evidence trail: For each output field, show which rules contributed to it and why. Create a table showing each field, its value, and the supporting rules.
  • Fact path navigation: Allow users to click on any fact in the output and see which rules read or write it. This creates an interactive exploration experience.
  • What-if analysis: Show how the decision would change if a specific input was different. This helps users understand decision boundaries (e.g., "If your income were $5,000 higher, you would be approved").

The monitor data (available in result.monitor and result.monitor_blocks) provides formatted text suitable for rendering in logs, web pages, or interactive tools. The support data provides structured information suitable for programmatic use. Together, they give you everything you need to build transparent, auditable systems.

15. Examples: SESL in business contexts

This section presents five business examples that show SESL in realistic situations. Each example consists of a scenario description, a model fragment, sample input data, and a short explanation of the engine output.

15.1 Insurance risk assessment

Scenario. An insurance company wants to flag high risk motor policies.

model: "Motor risk flags"

rules:
  - rule: "Flag young driver"
    if: "policy.driver_age < 25"
    then:
      result.risk_flags.young_driver: true
    reason: "Driver is younger than twenty five years"

  - rule: "Flag powerful car"
    if: "policy.engine_power_kw > 150"
    then:
      result.risk_flags.powerful_car: true
    reason: "Vehicle has high engine power"

  - rule: "Set overall risk band"
    let:
      has_any_flag: "result.risk_flags.young_driver or result.risk_flags.powerful_car"
    if: "has_any_flag"
    then:
      result.risk_band: "high"
    reason: "At least one high risk flag is present"

facts:
  - name: "Example policy"
    policy:
      driver_age: 22
      engine_power_kw: 160
    result: {}

Execution.

rules, scenarios = load_model_from_yaml(open("motor.sesl.yaml").read())
name, facts = scenarios[0]
monitor = Monitor()
forward_chain(rules, facts, monitor=monitor)
print(facts["result"])

Interpretation.

  • The first rule sets result.risk_flags.young_driver to true.
  • The second rule sets result.risk_flags.powerful_car to true.
  • The third rule sees that at least one flag is set and assigns the overall result.risk_band to "high".
  • The explanation data shows clearly which flags led to the high risk band.

15.2 Banking credit scoring

Scenario. A bank wants to compute a simple credit score.

model: "Credit score demo"

const:
  income_threshold: 30000
  low_debt_ratio: 0.3

rules:
  - rule: "Base score"
    then:
      result.score: 50
    reason: "Starting score"

  - rule: "Increase score for high income"
    if: "applicant.income >= const.income_threshold"
    then:
      result.score: "result.score + 20"
    reason: "Stable income"

  - rule: "Decrease score for high debt ratio"
    let:
      debt_ratio: "applicant.total_debt / max(applicant.income, 1)"
    if: "debt_ratio > 0.5"
    then:
      result.score: "result.score - 30"
    reason: "High debt compared with income"

facts:
  - name: "Applicant example"
    applicant:
      income: 40000
      total_debt: 15000
    result:
      score: 0

Execution and outcome.

  • The base score rule sets the score to fifty.
  • The high income rule adds twenty, bringing the score to seventy.
  • The debt ratio is fifteen thousand divided by forty thousand, which is zero point three seven five.
  • The debt ratio rule does not fire because the ratio is not greater than zero point five.
  • The final score is seventy.

15.3 Retail pricing and discounts

Scenario. A retailer wants to apply volume discounts.

model: "Volume discount"

rules:
  - rule: "Ten percent discount for ten or more items"
    if: "basket.quantity >= 10"
    then:
      result.discount_rate: 0.10
    reason: "Volume discount for ten or more items"

  - rule: "Fifteen percent discount for twenty or more items"
    priority: 20
    if: "basket.quantity >= 20"
    then:
      result.discount_rate: 0.15
    reason: "Higher volume discount for twenty or more items"

  - rule: "Compute total price"
    let:
      discounted_price_per_unit: "basket.unit_price * (1 - result.discount_rate)"
    then:
      result.total_price: "basket.quantity * discounted_price_per_unit"
    reason: "Apply discount rate to quantity and unit price"

facts:
  - name: "Basket example"
    basket:
      quantity: 22
      unit_price: 5.00
    result:
      discount_rate: 0.0

Interpretation.

  • Both discount rules match, but the higher priority rule that sets fifteen percent discount takes ownership of result.discount_rate.
  • The compute total price rule uses the final discount rate and writes the total price. With a quantity of twenty two and a unit price of five, the discounted price per unit is four point two five, giving a total price of ninety three point five.

15.4 Supply chain routing

Scenario. A logistics team wants to pick a warehouse to ship from.

model: "Warehouse selection"

rules:
  - rule: "Prefer local warehouse"
    if:
      all:
        - "order.destination_country == 'United Kingdom'"
        - "stock.local_available >= order.quantity"
    then:
      result.source_warehouse: "local"
    reason: "Destination is local and local stock is sufficient"

  - rule: "Fallback to regional warehouse"
    if: "result.source_warehouse is None and stock.regional_available >= order.quantity"
    then:
      result.source_warehouse: "regional"
    reason: "Local warehouse cannot fulfil the order"

facts:
  - name: "Order example"
    order:
      destination_country: "United Kingdom"
      quantity: 50
    stock:
      local_available: 40
      regional_available: 100
    result:
      source_warehouse: null

Outcome.

  • The local warehouse rule does not fire because local stock is only forty.
  • The regional warehouse rule fires and sets result.source_warehouse to "regional".
  • Explainability data records that the fallback rule provided the value, together with its reason text.

15.5 Compliance checking

Scenario. A compliance team wants to check whether a transaction breaches simple policy rules.

model: "Policy compliance"

const:
  maximum_single_transaction: 10000
  restricted_country_list: ["Country X", "Country Y"]

rules:
  - rule: "Flag large transaction"
    if: "transaction.amount > const.maximum_single_transaction"
    then:
      result.policy_flags.large_transaction: true
    reason: "Amount is greater than policy limit"

  - rule: "Flag restricted destination country"
    if: "transaction.country in const.restricted_country_list"
    then:
      result.policy_flags.restricted_country: true
    reason: "Destination country is on the restricted list"

  - rule: "Set compliance outcome"
    let:
      any_flag: "result.policy_flags.large_transaction or result.policy_flags.restricted_country"
    if: "any_flag"
    then:
      result.compliance_outcome: "review_required"
    reason: "At least one policy flag is present"

facts:
  - name: "Payment example"
    transaction:
      amount: 12000
      country: "Country X"
    result:
      policy_flags: {}

Outcome.

  • The large transaction rule fires because the amount is greater than ten thousand.
  • The restricted country rule also fires because the destination country is in the list.
  • The final outcome is result.compliance_outcome set to "review_required".
  • The monitor blocks show exactly which conditions and values led to this outcome, which helps when reviewing cases.

16. Best practices and common pitfalls

16.1 Organising models, rules, and facts

  • Use one SESL model per coherent decision area, such as one model for eligibility and another for pricing.
  • Group related rules near each other and use descriptive rule names that start with a verb, such as “Set tier” or “Flag high risk”.
  • Keep example fact scenarios small and focused. Each scenario should illustrate one main path through the rules.

16.2 Naming conventions

  • Use lower case and underscores for keys, such as total_debt rather than TotalDebt.
  • Use consistent namespaces such as applicant, customer, transaction, and result.
  • Use the result namespace only for outputs from rules, not for inputs. This keeps it clear which fields are derived.

16.3 Performance considerations

  • Avoid unnecessary complexity in conditions. If a condition becomes very long, consider computing parts of it in the let block.
  • Use priorities to avoid repeatedly overwriting the same path from many rules.
  • Monitor iteration counts and rule fire counts. Unexpectedly high values may indicate cycles or rules that constantly flip values back and forth.

16.4 Common pitfalls

  • Missing parent paths. When strict path mode is enabled and automatic path creation is disabled, attempting to write to result.score without having a result mapping leads to a helpful error. Seed result: {} in facts.
  • Unquoted string literals. In strict operand mode, text values must be quoted. Writing then: result.status: approved without quotes will cause a resolution error.
  • Self-referential let expressions. A helper variable cannot refer to itself. This is caught at compile time to prevent confusing behaviour.
  • Conflicting writes. Multiple rules that write to the same path at the same priority may conflict. The engine can warn or raise an error depending on configuration.

17. Conclusion and next steps

SESL provides a focused, transparent way to express decision logic as rules over structured data. You have seen how models are structured, how rules and facts interact, how the engine evaluates conditions and actions, and how to interpret the output and explanation data.

Possible next steps include:

  • Creating your own small model for a decision in your organisation.
  • Adding more example scenarios to test edge cases and ensure that rules behave as expected.
  • Integrating SESL into an application by building a wrapper that prepares facts and consumes the result and explanation structures.
  • Exploring the monitor and truth maintenance data to build user interfaces that explain decisions.

With careful model design and the engine features described in this guide, SESL can act as a reliable and understandable decision layer in many different domains.