rule-engine

Manifest

The Manifest is the entry point for a rule project. It is a single YAML file that ties everything together: it names the project, and lists one or more entries, each of which combines a field schema, an action schema, and a set of rule files.


Why a Manifest?

When working with more than a handful of rule files, it quickly becomes inconvenient to specify every path individually. The manifest provides a single place to describe a complete rule configuration — making it easy to load, validate, and run everything with one reference.

It also supports multiple entries, which lets you manage distinct rule sets (for example, transaction classification rules and fraud detection rules) inside a single project.


File Format

name: <project-name>

entries:
  - id: <entry-id>
    schema: <path-to-field-schema.yaml>
    actions: <path-to-action-schema.yaml>
    rules:
      - <path-to-rule-file.rule>
      - <path-to-another-rule-file.rule>

All paths are relative to the manifest file itself. This means you can move the entire project folder without changing any paths.


Fields

Top-level

Field Required Description
name optional A human-readable name for the project
entries List of rule set entries

Per Entry

Field Required Description
id Unique identifier for this entry
schema optional Relative path to the field schema YAML file
actions optional Relative path to the action schema YAML file
rules List of relative paths to .rule files. The list order defines execution order — rules are evaluated file by file in this order, then in declaration order within each file, and matches are returned in that order.
scope optional Name of a declared collection. The entry’s rules run once per member of it instead of once for the whole document.

Evaluating once per collection member

Without scope, a rule set is evaluated once against the whole document — the default, and what every manifest written before this key means.

entries:
  - id: account-review
    scope: accounts
    schema: schema.yaml
    rules:
      - rules/exposure.rule

A scoped entry’s rules are written from one member’s point of view and name the member’s own fields directly (balance, not accounts.balance). A name the member does not carry resolves against the document, so a rule can still read a shared threshold or watch list.

The result keeps matches as one flat list in member order, each tagged with the member it came from, and adds one members entry per member carrying that member’s own variables and stoppedBy — a stop ends one member’s run, not the fan-out.

Rejected at load time when scope names no field, or names a field that is not a collection.


Simple Example

name: transaction-rules

entries:
  - id: transactions
    schema: schemas/transaction-schema.yaml
    actions: schemas/actions.yaml
    rules:
      - rules/classification.rule
      - rules/fraud-detection.rule

With this manifest, the engine loads:


Multiple Entries

A manifest can contain multiple entries. Each entry is completely independent — it has its own schema, action schema, and rules. This is useful when different parts of a system operate on different data models or have separate rule sets.

name: banking-rules

entries:
  - id: transaction-classification
    schema: schemas/transaction-schema.yaml
    actions: schemas/transaction-actions.yaml
    rules:
      - rules/transactions/classification.rule
      - rules/transactions/chargebacks.rule

  - id: customer-risk
    schema: schemas/customer-schema.yaml
    actions: schemas/customer-actions.yaml
    rules:
      - rules/customers/risk-scoring.rule
      - rules/customers/vip-detection.rule

Each entry is loaded and evaluated independently.


Rule Order

The order of the rules: list is the order the engine evaluates in — this list first, then the order the rules are declared inside each file. That is a guarantee, not an implementation detail.

For plain rules the order is only visible in the results: every rule is checked against every record, and the order decides in which order the matches come back.

Two constructs make it part of the meaning, so that reordering the list changes the outcome rather than just its sequence.

A variable: a set clause makes its value available to the rules after it in this list.

entries:
  - id: orders
    schema: schema.yaml
    actions: actions.yaml
    rules:
      # First: publishes $orderTotal, which the rules below read.
      - rules/totals.rule
      - rules/pricing.rule
      - rules/routing.rule

Moving totals.rule down would make every $orderTotal read a forward reference, and the build would fail with reads unknown variable '$orderTotal'.

Variables are scoped to a single entry and a single evaluation. Two entries never see each other’s variables, and nothing carries over from one input record to the next.

The visual editor follows the same list. With a manifest open it validates the file you are editing against the variables every file listed before it publishes, so a final rule file whose whole job is to read what the earlier files accumulated validates in the editor exactly as it does at load time. A file’s own forward reference is still reported: a set in a file listed after the open one is not in scope there either.

A stop makes the order load-bearing in a second way: the rules listed after the rule that stops are not evaluated at all. Move a guard rule down and it silently stops guarding the rules that were above it. Comment the intent, the same way as for a variable:

entries:
  - id: orders
    schema: schema.yaml
    actions: actions.yaml
    rules:
      # First: sanctioned-country guard, ends the run — everything below assumes it passed.
      - rules/sanctions.rule
      - rules/pricing.rule

Here is a clean folder structure for a rule project using a manifest:

my-rules/
├── manifest.yaml
├── schemas/
│   ├── transaction-schema.yaml
│   └── actions.yaml
└── rules/
    ├── classification.rule
    ├── fraud-detection.rule
    └── chargebacks.rule

The manifest.yaml would then look like:

name: my-rules

entries:
  - id: transactions
    schema: schemas/transaction-schema.yaml
    actions: schemas/actions.yaml
    rules:
      - rules/classification.rule
      - rules/fraud-detection.rule
      - rules/chargebacks.rule

Real-World Example

The following is the manifest used in the full example project:

name: full-sample

entries:
  - id: full
    schema: full-schema.yaml
    actions: actions.yaml
    rules:
      - rules/rent.rule
      - rules/vip.rule
      - rules/fraud.rule

And for the KLS legal-affairs use case:

name: kls

entries:
  - id: legal_affairs
    schema: fields_transaction.yaml
    actions: actions_transaction.yaml
    rules:
      - label_de_legal_affairs_1.rule

Validation

When the engine loads a manifest, it performs the following checks:

If any check fails, the engine reports detailed errors and does not start.

You can run the same checks without starting anything, with ValidatorCli in manifest mode:

java -cp "<runtime classpath>" ruleengine.cli.ValidatorCli \
  --manifest rules/manifest.yaml [--entry <id>] [--format json]

It validates the entry file by file in manifest order, so each diagnostic names the rule file it came from and every variable resolves exactly as it will at load time. Prefer it over --schema + --rules for anything that has a manifest: the directory mode cannot know the file order, and checks action names only when you also pass --actions.


Tips and Best Practices