Writing rules¶
Fathom rules are YAML files that compile to CLIPS defrule constructs. This guide covers
the full structure of a ruleset file, each field that the compiler accepts, and the
validation constraints enforced by the Pydantic models.
Rule skeleton¶
Every rule file is a single YAML document that matches the RulesetDefinition model
(src/fathom/models.py).
ruleset: access-control # unique name — CLIPS identifier chars only
version: "1.0" # free string; no runtime effect
module: governance # CLIPS module all rules belong to
rules:
- name: deny-low-clearance
description: "Deny when agent clearance is below data classification"
salience: 10
when:
- template: agent
conditions:
- slot: clearance
expression: equals(unclassified)
then:
action: deny
reason: "Clearance insufficient"
The four top-level keys map directly to RulesetDefinition fields:
| Field | Type | Notes |
|---|---|---|
ruleset |
string | CLIPS identifier — [A-Za-z_][A-Za-z0-9_-]* |
version |
string | Defaults to "1.0" |
module |
string | CLIPS identifier; routes rules to a CLIPS module |
rules |
list | One or more RuleDefinition objects |
Each rule inside rules is a RuleDefinition and must supply name, when, and then.
description and salience are optional (salience defaults to 0).
when clauses: FactPattern¶
Each entry in when is a FactPattern (src/fathom/models.py). It matches a single
fact type in working memory.
when:
- template: data-request # name of the deftemplate to match
alias: $req # optional — names this pattern for $req.slot references
conditions:
- slot: action
expression: equals(read)
| Field | Required | Notes |
|---|---|---|
template |
yes | Must match a loaded TemplateDefinition name |
alias |
no | $-prefixed name for this pattern, referenced from another pattern's expression as $alias.slot. Must not be $p<number>, and no two patterns in a rule may share one |
conditions |
yes | List of ConditionEntry objects |
A rule with two when entries fires only when both facts exist simultaneously in
working memory — CLIPS evaluates the conjunction.
ConditionEntry fields¶
ConditionEntry (src/fathom/models.py, line 203) represents one slot constraint
inside a FactPattern. At least one of expression, bind, or test must be present.
slot + expression: value match¶
An expression is always one operator(argument) form — a bare value is
rejected by the model, not silently treated as equals.
slot is required when expression is set. For the full operator list, the
argument forms ($alias.slot cross-references, [a, b] lists, literals), and
how a literal is emitted for each slot type, see the
Rule reference.
slot + bind: capture a value¶
Use bind to capture a slot value into a CLIPS variable for use in other conditions or
the then block. The value must be ? followed by a CLIPS identifier
(^\?[A-Za-z_][A-Za-z0-9_\-]*$) — the validator rejects anything else, because the
name is interpolated straight into the generated defrule.
slot is required when bind is set.
test: standalone CLIPS expression¶
Use test for arbitrary CLIPS conditional elements — custom functions registered via
Engine.register_function, or any CLIPS built-in not in Fathom's operator allow-list.
The value must be a parenthesized expression (start with (, end with )).
The compiler emits (test (my-fn ?sid)) on the rule LHS after all slot patterns.
You can combine bind and test in the same ConditionEntry to both capture a slot
and run a test against it:
Validator rules enforced by ConditionEntry:
bindmust be?followed by a CLIPS identifier — e.g.?sid, notsidand not?a b.expressionmust be a singleoperator(argument)form — e.g.equals(active), notactive.testmust be a parenthesized CLIPS expression — e.g.(my-fn ?sid).slotmust be present whenexpressionorbindis set.- Setting
slotalongsidetestalone (noexpressionorbind) is rejected — the compiler has no slot position to emit; either addexpression/bindor dropslot. - At least one of
expression,bind, ortestmust be set. - Any key other than
slot,expression,bind, andtestis rejected. This holds for every model on this page: a misspelled field is an error, never a silently ignored line.
then block: ThenBlock¶
The then block is a ThenBlock (src/fathom/models.py). It declares the decision and
any side effects when the rule fires.
then:
action: deny
reason: "Subject ?sid is not authorized"
log: full
notify: [security-ops]
attestation: true
metadata:
control: AC-3
assert:
- template: audit-record
slots:
subject: "?sid"
outcome: denied
| Field | Type | Notes |
|---|---|---|
action |
ActionType or null |
The decision outcome (see below) |
reason |
string | Human-readable explanation; defaults to "" |
log |
LogLevel |
none, summary (default), or full |
notify |
list of strings | Channel names to notify |
attestation |
bool | Whether to produce an attestation token |
metadata |
dict | Arbitrary string key-value pairs |
scope |
string or null | Scope qualifier for scope actions |
assert |
list of AssertSpec |
Facts to assert when the rule fires |
Either action or a non-empty assert list is required — a ThenBlock with neither
is rejected by the model validator.
Note: in YAML files use the key assert; in Python you may use the attribute name
asserts (the model sets populate_by_name=True).
assert examples¶
AssertSpec (src/fathom/models.py) lets rules write new facts into working memory.
Slot values starting with ? are emitted as CLIPS variable references; values starting
with ( are emitted as CLIPS s-expressions; all other values are emitted as quoted
string literals.
assert:
- template: decision
slots:
subject: "?sid" # ?-prefixed → CLIPS variable reference
outcome: "denied" # plain string → CLIPS quoted literal
score: "(compute-score ?sid)" # (...) → CLIPS s-expression
Validators on AssertSpec:
templatemust be a valid CLIPS identifier.- Slot names must be valid CLIPS identifiers.
?-prefixed values must be valid CLIPS variable references (e.g.?sid).(-prefixed values must have balanced parentheses.
Salience overview¶
Salience is an integer on each RuleDefinition that controls firing order within a
module. Higher salience fires first. When salience is omitted it defaults to 0.
Under Fathom's last-write-wins convention for the __fathom_decision fact, the rule
that fires last sets the final decision. This means deny rules should be given
lower salience so they fire after allow rules and their outcome wins. See
Modules and salience for a full worked example
and focus-stack ordering.
Action values¶
The action field accepts any value from the ActionType enum
(src/fathom/models.py):
| Value | Meaning |
|---|---|
allow |
Permit the request |
deny |
Reject the request |
escalate |
Route to a human or higher-authority system |
scope |
Narrow the permission to a specific scope |
route |
Redirect to an alternate handler or service |