A rule-based system represents knowledge as a set of IF condition THEN action production rules and uses an inference engine to apply them to a working memory of facts. It is the classic implementation style for expert systems and lives on in business rules engines (Drools, CLIPS, decision tables), fraud and eligibility rules, alerting systems, linters and policy-as-code tools.
Part of Knowledge & Reasoning. Compare with richer representations in Knowledge-based Systems and the formal foundation in Logic & Inference.
A production system has three parts: a rule base of productions, a working memory of facts (working memory elements, WMEs), and an interpreter that repeats a recognize-act cycle. In each cycle it finds every rule whose conditions are satisfied by current facts, chooses one from that conflict set, and executes its action, which usually adds, modifies or removes facts. The changed facts can enable new rules, so behavior emerges from chains of small, independent rules rather than a hand-written control flow.
Forward chaining starts from known facts and fires rules until nothing new can be derived. It is natural for event processing, monitoring, configuration and any setting where facts arrive and you want all consequences. CLIPS, Drools and OPS5 are forward chainers.
Backward chaining starts from a goal and works backward: to prove approve_loan, find rules that conclude it, then try to prove their premises recursively, asking the user when a fact is unknown. It avoids deriving irrelevant facts and naturally produces focused questions, which is why MYCIN and Prolog use it. Many engines support both.
When several rules match, the engine needs a deterministic policy for which fires first. Common strategies, often combined:
Decision tables in DMN formalize the same idea through hit policies such as unique, first, priority and collect.
Naively re-matching every rule against every fact on every cycle is far too slow for thousands of rules and facts. Charles Forgy's Rete algorithm compiles conditions into a dataflow network. Alpha nodes test single-fact conditions and keep memories of passing facts; beta nodes join facts across conditions and cache partial matches. When a fact is added or removed, only that change flows through the network. Rete trades memory for speed and exploits two observations: working memory changes slowly between cycles, and many rules share conditions. Successors such as TREAT, LEAPS and Drools' PHREAK refine the trade-off, for example by evaluating lazily.
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class Rule:
name: str
when: Callable[[dict], bool]
then: Callable[[dict], dict] # returns new/updated facts
salience: int = 0
@dataclass
class Engine:
rules: list
fired: set = field(default_factory=set)
log: list = field(default_factory=list)
def run(self, facts: dict, max_cycles: int = 100) -> dict:
for _ in range(max_cycles):
snapshot = tuple(sorted(facts.items()))
agenda = [r for r in self.rules
if (r.name, snapshot) not in self.fired and r.when(facts)]
if not agenda:
return facts # quiescence
rule = max(agenda, key=lambda r: r.salience) # conflict resolution
self.fired.add((rule.name, snapshot)) # refraction
updates = rule.then(facts)
self.log.append((rule.name, updates))
facts = {**facts, **updates}
raise RuntimeError("possible rule loop")
rules = [
Rule("dti", lambda f: "dti" not in f,
lambda f: {"dti": f["debt"] / f["income"]}),
Rule("low_dti", lambda f: "dti" in f and "low_dti" not in f,
lambda f: {"low_dti": f["dti"] < 0.36}),
Rule("good_cred", lambda f: "good_credit" not in f,
lambda f: {"good_credit": f["score"] >= 700}),
Rule("approve", lambda f: f.get("low_dti") and f.get("good_credit")
and "decision" not in f,
lambda f: {"decision": "approve"}, salience=10),
Rule("refer", lambda f: "low_dti" in f and "good_credit" in f
and "decision" not in f,
lambda f: {"decision": "manual_review"}, salience=5),
]
eng = Engine(rules)
print(eng.run({"income": 9000, "debt": 2500, "score": 730}))
for name, upd in eng.log:
print(f" fired {name:10s} -> {upd}")
The log is the explanation trace. For production use, Python options include durable_rules and experta (a CLIPS-like Rete engine), or call out to Drools, OPA (Rego) or a DMN engine.
Consider four rules for a small equipment-diagnosis system:
no_power AND plugged_in THEN check_fusecheck_fuse AND fuse_blown THEN replace_fuseno_power AND NOT plugged_in THEN plug_inreplace_fuse THEN log_maintenanceForward chaining from facts {no_power, plugged_in, fuse_blown}: cycle 1 matches only R1, which adds check_fuse. Cycle 2 matches R2 (R1 is suppressed by refraction), adding replace_fuse. Cycle 3 matches R4, adding log_maintenance. Cycle 4 finds no new activations and the engine halts. Every consequence has been derived, including the logging action nobody explicitly asked for — exactly what you want in monitoring and event processing.
Backward chaining with goal replace_fuse: the engine finds R2 concludes it, so it needs check_fuse and fuse_blown. check_fuse is concluded by R1, which needs no_power and plugged_in; if those are not already known, the system asks the user. fuse_blown is concluded by no rule, so it too becomes a question. R3 and R4 are never examined. The questions asked are exactly the ones relevant to the goal, which is why diagnostic and advisory expert systems favour this direction.
The NOT in R3 is worth noticing. Rule engines usually interpret it under the closed-world assumption: "not known to be true" is treated as false. That is convenient but can surprise you when facts arrive late — a rule that fired on the absence of a fact may need to be retracted when the fact appears, which is what truth-maintenance features in engines such as Drools (logical insertion) handle.
Modern decision systems rarely choose between rules and models; they layer them. Hard rules encode eligibility and regulatory constraints that must hold regardless of any score (age limits, sanctioned countries, required documents). A model ranks the remaining cases by risk or likelihood. Policy rules then turn the score into an action, add overrides (manual review above a certain amount), and record which rules fired. This pattern appears in credit decisioning, fraud screening, insurance underwriting, and clinical alerting.
Rules also generate training data and features: rule outputs can label data for weak supervision, and a model's predictions can be distilled back into readable rules (decision trees or rule lists) when an explanation must be stated in business terms. See Decision Trees for the most direct bridge between the two worlds, and Fuzzy Logic for rules with graded rather than true/false conditions.
| Technology | Style | Typical use |
|---|---|---|
| CLIPS, Drools, Jess | Forward-chaining production rules (Rete family) | Business rules, configuration, event processing |
| Prolog, Datalog | Logic programming, backward chaining or bottom-up evaluation | Deductive queries, program analysis, logical inference |
| DMN decision tables | Tabular rules with hit policies | Business-owned decisions in BPM platforms |
| OPA / Rego | Declarative policy over JSON | Authorization, infrastructure policy |
| Complex event processing (Esper, Flink CEP) | Patterns over event streams and time windows | Fraud, monitoring, IoT alerts |