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: 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 — bind the whole fact to a CLIPS variable
conditions:
- slot: action
expression: read
| Field | Required | Notes |
|---|---|---|
template |
yes | Must match a loaded TemplateDefinition name |
alias |
no | When set, the compiler emits ?alias <- (template ...) |
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, lines 105–171) represents one slot constraint
inside a FactPattern. At least one of expression, bind, or test must be present.
slot + expression: value match¶
Use expression to require an exact slot value (compiled to a CLIPS equality
constraint).
slot is required when expression is set.
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 start with ? — the validator rejects anything else.
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 start with?— e.g.?sid, notsid.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.
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 |