Rule language reference
The exact grammar of the rule language, how a rulebook is evaluated and decided, every annotation it accepts, and what the engine and a digital worker do with each one. Every example on this page is a rulebook file, and the output under it is the recorded run of that file.
For a walkthrough with a complete 35-rule rulebook, read the guide page Rule language first. This page is the detail behind it.
How to read the examples#
Each example is one rulebook file. Lines that start with # case, # requires or # report are comments to the language; the example runner reads them as instructions. For every file the runner mounts the rulebook in a digital worker, which loads it through the rule engine (parse, normalize, lower, validate; any error stops the load and lists every problem), then sends each case through the worker's decision path. A rulebook that names @ontology example_credit_ops is mounted with that ontology. The output was recorded from that run, and a test fails when this page and a fresh run differ.
| Line in the file | What the runner does |
|---|---|
# case ID: {...} | Runs one case. The JSON object holds the case facts. |
# requires: a, b | The decision type needs facts a and b. A case without one of them gets NEED_DATA before any rule runs. |
# report: stored | Prints what each rule stores, compared with the same rule line without its annotations, and what the worker's rulebook view shows for it. |
# report: readiness | Prints the engine's readiness check for each case: the required facts of each rule and which are missing. |
# report: policy | Prints the worker's pipeline policy after loading. |
# report: vocabulary | Prints the rulebook's ontology and how many case facts the worker typed against it. |
In a recorded run, fired lists the rules that fired in the order they fired, derived facts the conclusions that are not recommendations or deontic forms, recommendations every RECOMMEND_ conclusion, not fired each rule that could not fire and why (inactive, not in effect, out of jurisdiction, suppressed by an exception, blocked by a missing required fact, undetermined because a value is unknown, or a violated constraint), engine decision the engine's outcome with the reason it won, and outcome the worker's decision: APPROVE, HOLD, DECLINE or NEED_DATA. The rulebook view is what the worker returns when you read a mounted rulebook.
Grammar#
A rulebook is a text file of lines. Each line is blank, a comment, an annotation or a rule.
rulebook = { line } ;
line = blank | comment | annotation | rule ;
comment = "#" text ; (at the start of a line, or after whitespace and followed by whitespace)
annotation = "@" name [ ":" | "=" ] [ value ] ; (applies to the next rule only)
rule = [ rule-id ":" ] body ;
rule-id = letter { letter | digit | "_" | "-" } ;
body = "IF" alternatives "THEN" conclusions
| alternatives "IMPLIES" conclusions
| conditions "<->" conclusions (expands to two rules)
| "EXACTLY_ONE" "(" condition { "," condition } ")"
| "AT_MOST_K" "(" k "," condition { "," condition } ")" ;
alternatives = conditions { "OR" conditions } ; (one rule per alternative)
conditions = condition { "AND" condition } ;
condition = [ "NOT" ] ( fact | comparison ) ;
comparison = expression operator expression ;
expression = fact | number | code | parameter | expression ( "+" | "-" | "*" | "/" ) expression
| "(" expression ")" ;
operator = "==" | "!=" | ">" | ">=" | "<" | "<=" ;
conclusions = conclusion { "AND" conclusion } ;
conclusion = fact | deontic "(" target { "," argument } ")" ;
deontic = "OBLIGATION" | "PROHIBITION" | "PERMISSION"
| "EXCEPTION" | "SAFE_HARBOR" | "DISCLOSURE_REQUIRED" ;
Keywords (IF, THEN, AND, OR, NOT, IMPLIES) are not case sensitive. Fact names are upper-cased, and any run of characters other than letters and digits becomes _. Rule ids are kept exactly as written.
A rule and a comment#
One rule per line. A # starts a comment at the start of a line, or at the end of a line when it has a space before and after it. A rule whose fact is missing is blocked, and when that leaves the case without a decision the outcome is NEED_DATA with the missing fact (case score-missing).
# One rule per line: an id, a colon, IF conditions THEN conclusions.
# Everything after # is a comment, on its own line or at the end of a line.
# case score-known: {"credit_score": 700}
# case score-missing: {"years_in_business": 4}
G01: IF HAS_CREDIT_SCORE THEN RECOMMEND_APPROVE # holds when the credit score is known
loaded 1 rule: G01 case score-known: credit_score=700 fired: G01 derived facts: none recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): G01 recommends APPROVE outcome: APPROVE case score-missing: years_in_business=4 fired: none derived facts: none recommendations: none not fired: G01 blocked (required facts missing: HAS_CREDIT_SCORE) engine decision: NEED_DATA (missing_data): no rule recommended a decision; missing HAS_CREDIT_SCORE stopped G01, which can lead to a recommendation (G01) outcome: NEED_DATA, missing credit_score
AND and NOT#
All conditions joined by AND must hold. NOT X holds when no rule derives X in the run and X is not a true case fact. The engine evaluates a rule that uses NOT X only after every rule that can derive X has run, so the answer never depends on file order (see Evaluation order). NOT of something unknown is unknown (see NOT of a comparison). OR is described with its example in OR.
# AND joins conditions; all must hold. NOT X holds when X is not derived and the case fact behind it is not true.
# NOT of something unknown (a comparison on a fact the case lacks) is unknown, so the rule does not fire.
# case clean: {"credit_score": 700, "sanctions_hit": 0}
# case match: {"credit_score": 700, "sanctions_hit": 1}
G02A: IF HAS_CREDIT_SCORE AND HAS_SANCTIONS_HIT == 0 THEN SCREENED_CLEAN
G02B: IF SCREENED_CLEAN AND NOT MANUAL_REVIEW THEN RECOMMEND_APPROVE
G02C: IF HAS_CREDIT_SCORE AND NOT SCREENED_CLEAN THEN RECOMMEND_HOLD
loaded 3 rules: G02A G02B G02C case clean: credit_score=700, sanctions_hit=0 fired: G02A G02B derived facts: SCREENED_CLEAN recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): G02B recommends APPROVE outcome: APPROVE case match: credit_score=700, sanctions_hit=1 fired: G02C derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): G02C recommends HOLD outcome: HOLD
Comparisons#
A comparison is a case fact, one of six operators, and a number, a code, a parameter or another case fact. Either side may compute with +, -, *, / and parentheses (see Arithmetic). The rules for how values compare are in Values; worked examples, including NOT of a comparison, are in Comparisons.
| Operator | Holds when the fact's value is |
|---|---|
== | equal to the right-hand side |
!= | not equal to the right-hand side |
>, >= | greater than, or greater than or equal to, a number |
<, <= | less than, or less than or equal to, a number |
Conclusions#
A conclusion is a fact the rule derives. RECOMMEND_APPROVE, RECOMMEND_HOLD and RECOMMEND_DECLINE are the conclusions that decide; any other RECOMMEND_ name stops the rulebook from loading. A deontic form OBLIGATION(x) concludes OBLIGATION_X, and the same for the other five forms. Join several conclusions with AND. A conclusion cannot be negated, compare, assign a value, or name a case fact (HAS_, KNOWN_); see Rejected forms.
# A conclusion is a derived fact, a recommendation, or a deontic form; join several with AND.
# case large: {"requested_amount": 400000, "customer_years": 12}
# case small: {"requested_amount": 90000, "customer_years": 2}
G03A: IF HAS_REQUESTED_AMOUNT > 250000 THEN LARGE_EXPOSURE AND RECOMMEND_HOLD AND OBLIGATION(credit_committee_review) AND PROHIBITION(auto_fund)
G03B: IF HAS_CUSTOMER_YEARS >= 10 THEN PERMISSION(waive_appraisal_fee)
G03C: IF HAS_REQUESTED_AMOUNT <= 250000 THEN RECOMMEND_APPROVE
loaded 3 rules: G03A G03B G03C case large: requested_amount=400000, customer_years=12 fired: G03A G03B derived facts: LARGE_EXPOSURE recommendations: RECOMMEND_HOLD obligations and prohibitions: OBLIGATION_CREDIT_COMMITTEE_REVIEW; PROHIBITION_AUTO_FUND; PERMISSION_WAIVE_APPRAISAL_FEE engine decision: HOLD (only_candidate): G03A recommends HOLD outcome: HOLD case small: requested_amount=90000, customer_years=2 fired: G03C derived facts: none recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): G03C recommends APPROVE outcome: APPROVE
Rule ids and other rule forms#
A rule id starts with a letter and continues with letters, digits, _ or -. A rule without an id gets RULE_ and its line number. A IMPLIES B is the same as IF A THEN B. A <-> B becomes two rules, ID__1 (A gives B) and ID__2 (B gives A). Lower-case rules work: names are upper-cased, ids are not.
# Rule ids: a letter, then letters, digits, _ or -. A line without an id gets RULE_<line number>.
# Keywords and fact names are not case sensitive; the id is kept as written.
# A IMPLIES B is the same as IF A THEN B. A <-> B becomes two rules, one each way.
# case young: {"years_in_business": 1}
credit-young_01: IF HAS_YEARS_IN_BUSINESS < 2 THEN YOUNG_BUSINESS
IF YOUNG_BUSINESS THEN ELEVATED_RISK
G04C: ELEVATED_RISK IMPLIES RECOMMEND_HOLD
G04D: THIN_FILE <-> NEW_TO_BANK
g04e: if young_business and not thin_file then first_year_review
loaded 6 rules: credit-young_01 RULE_7 G04C G04D__1 G04D__2 g04e case young: years_in_business=1 fired: credit-young_01 RULE_7 G04C g04e derived facts: YOUNG_BUSINESS ELEVATED_RISK FIRST_YEAR_REVIEW recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): G04C recommends HOLD outcome: HOLD
Annotation lines#
An annotation is @name value on its own line, before the rule it describes. @name: value and @name = value mean the same, and the name is not case sensitive. An annotation applies to the next rule only and is then cleared, so every rule repeats the annotations it needs. An annotation with no value is stored as true. An annotation after the last rule is dropped. Values the engine acts on (@status, @authority, @priority, the effective dates) are checked when the rulebook loads (see Errors).
# An annotation line is @name value. It applies to the next rule only, then is cleared. # @name: value and @name = value mean the same. The name is not case sensitive. # An annotation with no value is stored as true. # report: stored @Owner: credit-policy @category = knockout @manual_review G05A: IF HAS_SANCTIONS_HIT == 1 THEN RECOMMEND_HOLD G05B: IF HAS_CREDIT_SCORE >= 650 THEN RECOMMEND_APPROVE
loaded 2 rules: G05A G05B G05A stores (compared with the same rule without annotations): metadata.owner = "credit-policy" metadata.category = "knockout" metadata.manual_review = true G05A in the worker's rulebook view: annotations.owner = "credit-policy" annotations.category = "knockout" G05B stores (compared with the same rule without annotations): nothing extra G05B in the worker's rulebook view: nothing extra
How case facts become tokens#
Every case fact name with a value reaches the rules twice. HAS_NAME carries the value: a plain HAS_NAME condition holds when the value is true, a number other than 0, or other text, and does not hold for 0, false, "false" and "0". KNOWN_NAME holds whenever the fact has a value, whatever it is. Fact names are matched the way rules write them: upper-cased, with any run of other characters turned into _, so credit-score, Credit Score and credit_score are the same fact. Two names in one case that come out the same are an error. A fact sent as null or as blank text is missing: it produces no token and counts as missing in every required-facts check.
# Each case fact <name> reaches the rules twice: HAS_<NAME> carries the value, KNOWN_<NAME> says it has one.
# A plain HAS_<NAME> condition is false for 0, false, "false" and "0"; KNOWN_<NAME> is true for any value.
# Names match whatever their case, hyphens or spaces. null and blank text are missing.
# case as-sent: {"credit_score": 700, "sanctions_hit": 0, "industry_risk": "HIGH", "audited_financials": true}
# case other-spellings: {"Credit_Score": 700, "sanctions_hit": false, "industry_risk": "high", "audited_financials": "true"}
# case hyphenated-name: {"credit-score": 700}
# case null-value: {"credit_score": null}
G06A: IF HAS_CREDIT_SCORE THEN SCORE_POSITIVE
G06B: IF HAS_SANCTIONS_HIT THEN SANCTIONS_FLAGGED
G06C: IF KNOWN_SANCTIONS_HIT THEN SCREENING_DONE
G06D: IF HAS_INDUSTRY_RISK == HIGH THEN HIGH_RISK_INDUSTRY
G06E: IF HAS_AUDITED_FINANCIALS == 1 THEN AUDITED
G06F: IF KNOWN_CREDIT_SCORE THEN RECOMMEND_APPROVE
loaded 6 rules: G06A G06B G06C G06D G06E G06F case as-sent: credit_score=700, sanctions_hit=0, industry_risk="HIGH", audited_financials=true fired: G06A G06C G06D G06E G06F derived facts: SCORE_POSITIVE SCREENING_DONE HIGH_RISK_INDUSTRY AUDITED recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): G06F recommends APPROVE outcome: APPROVE case other-spellings: Credit_Score=700, sanctions_hit=false, industry_risk="high", audited_financials="true" fired: G06A G06C G06D G06E G06F derived facts: SCORE_POSITIVE SCREENING_DONE HIGH_RISK_INDUSTRY AUDITED recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): G06F recommends APPROVE outcome: APPROVE case hyphenated-name: credit-score=700 fired: G06A G06F derived facts: SCORE_POSITIVE recommendations: RECOMMEND_APPROVE not fired: G06B blocked (required facts missing: HAS_SANCTIONS_HIT); G06D blocked (required facts missing: HAS_INDUSTRY_RISK); G06E blocked (required facts missing: HAS_AUDITED_FINANCIALS) engine decision: APPROVE (only_candidate): G06F recommends APPROVE outcome: APPROVE case null-value: credit_score=null fired: none derived facts: none recommendations: none not fired: G06A blocked (required facts missing: HAS_CREDIT_SCORE); G06B blocked (required facts missing: HAS_SANCTIONS_HIT); G06D blocked (required facts missing: HAS_INDUSTRY_RISK); G06E blocked (required facts missing: HAS_AUDITED_FINANCIALS) engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD
Values#
| Situation | What happens |
|---|---|
| Both sides read as numbers | Compared as numbers. A case value of "742" counts as a number; true, false, "true" and "false" count as 1 and 0. |
| Text | Codes compare like fact names: upper-cased, other characters as _. == high, == HIGH and a case value of "High" all match. |
| Text against a number | == is false and != is true. > and the other orderings stop the case with an evaluation error: they need numbers. |
| A case fact on the right | HAS_REQUESTED_AMOUNT > HAS_APPROVED_LIMIT compares the two values. |
| The fact is missing | The comparison is unknown, and so is its NOT: the rule does not fire and the fact is reported missing. See NOT of a comparison. |
| A derived fact | Derived facts carry no value. A comparison on one stops the rulebook from loading; test it as a plain condition. |
Missing, 0 and false | Missing (absent, null, blank text): no token, KNOWN_ false, comparisons unknown. 0 or false: KNOWN_ true, plain HAS_ false, comparisons use the value. |
Evaluation order#
File order does not matter. The engine sorts rules into layers when it loads the rulebook: a rule that uses NOT X, or that has an exception rule, goes in a later layer than every rule that can derive X or that is its exception. Within a layer, rules repeat until nothing new is derived, so chains of any length complete. A rule fires at most once and a conclusion is never taken back. Rules that depend on each other through NOT or @exceptions cannot be layered, and the rulebook does not load. This file is written backwards:
# File order does not matter. Each rule that uses NOT waits until everything it negates is final,
# and rules repeat until nothing new is derived. Here every rule is written before the rule it depends on.
# case thin-and-weak: {"debt_service_coverage": 1.1, "credit_score": 600}
G07A: IF HAS_DEBT_SERVICE_COVERAGE >= 1.0 AND NOT THIN_CASHFLOW THEN CASHFLOW_OK
G07B: IF HAS_DEBT_SERVICE_COVERAGE < 1.25 THEN THIN_CASHFLOW
G07C: IF COMPOUND_RISK THEN RECOMMEND_HOLD
G07D: IF THIN_CASHFLOW AND WEAK_CREDIT THEN COMPOUND_RISK
G07E: IF HAS_CREDIT_SCORE < 650 THEN WEAK_CREDIT
loaded 5 rules: G07A G07B G07C G07D G07E case thin-and-weak: debt_service_coverage=1.1, credit_score=600 fired: G07B G07E G07D G07C derived facts: THIN_CASHFLOW WEAK_CREDIT COMPOUND_RISK recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): G07C recommends HOLD outcome: HOLD
G07A waits for THIN_CASHFLOW and does not fire. The same rules in dependency order give the same run:
# The same rules in dependency order give the same result.
# case thin-and-weak: {"debt_service_coverage": 1.1, "credit_score": 600}
G08B: IF HAS_DEBT_SERVICE_COVERAGE < 1.25 THEN THIN_CASHFLOW
G08E: IF HAS_CREDIT_SCORE < 650 THEN WEAK_CREDIT
G08A: IF HAS_DEBT_SERVICE_COVERAGE >= 1.0 AND NOT THIN_CASHFLOW THEN CASHFLOW_OK
G08D: IF THIN_CASHFLOW AND WEAK_CREDIT THEN COMPOUND_RISK
G08C: IF COMPOUND_RISK THEN RECOMMEND_HOLD
loaded 5 rules: G08B G08E G08A G08D G08C case thin-and-weak: debt_service_coverage=1.1, credit_score=600 fired: G08B G08E G08D G08C derived facts: THIN_CASHFLOW WEAK_CREDIT COMPOUND_RISK recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): G08C recommends HOLD outcome: HOLD
Two rules that each require the other not to fire:
# Rules that depend on each other through NOT cannot be ordered, so the rulebook does not load.
# case any: {"credit_score": 700}
G11A: IF HAS_CREDIT_SCORE AND NOT MANUAL_REVIEW THEN AUTO_PATH
G11B: IF HAS_CREDIT_SCORE AND NOT AUTO_PATH THEN MANUAL_REVIEW
load failed: Rulebook ref-grammar-g11-cycle@v1 failed to load: (rulebook): negation_cycle: rules depend on each other through NOT or @exceptions, so no order can evaluate them: G11A, G11B (rulebook): negation_cycle no rule from this file is mounted
Names that start with NOT_#
Only the keyword NOT negates. NOT_REVIEWED is an ordinary fact name, in a condition and in a conclusion.
# Only the keyword NOT negates. A name that starts with NOT_ is an ordinary fact name.
# case any: {"credit_score": 700}
G10A: IF HAS_CREDIT_SCORE >= 650 THEN NOT_REVIEWED
G10B: IF NOT_REVIEWED THEN RECOMMEND_HOLD
G10C: IF NOT REVIEWED THEN NOTHING_REVIEWED
loaded 3 rules: G10A G10B G10C case any: credit_score=700 fired: G10A G10B G10C derived facts: NOT_REVIEWED NOTHING_REVIEWED recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): G10B recommends HOLD outcome: HOLD
From conclusions to a decision#
- Required facts of the decision type
If the case lacks a fact the decision type lists (absent,
nullor blank), the outcome isNEED_DATAwith the missing names, and no rule runs. - Which rules can fire
A rule with an inactive
@status, an effective window that does not contain the case's decision date, or a@jurisdictionother than the case's never fires. A rule whose required facts are missing is blocked. - Rules
The rulebook runs layer by layer as above. A rule fires when every condition is true; a condition on a missing value is unknown and the rule does not fire.
- Constraints
A violated
EXACTLY_ONEorAT_MOST_Kends the run asCONSTRAINT_VIOLATION(see Constraints). - Resolution
Among the rules that recommended something, the highest
@authoritywins, then the highest@priority, then the more conservative outcome: decline beats hold beats approve. The report lists every competing rule, the winners, the overridden rules and the reason (see Conflicting recommendations). - Missing data
If a rule blocked or left unknown by missing data could lead to a recommendation that would win against the result, or if nothing was recommended and such a rule could lead to any recommendation, the outcome is
NEED_DATAwith the facts that are missing.NEED_DATAmeans exactly this: missing data stopped a rule that could change the decision.
| Engine outcome | Meaning | Digital worker |
|---|---|---|
APPROVE, HOLD, DECLINE | A recommendation won. | The same outcome. |
NEED_DATA | Missing data stopped a rule that could change the decision. | NEED_DATA, with the missing case facts. |
NO_DECISION | No rule recommended anything, and no missing data could change that. | HOLD: held for a person, never approved by default. |
CONSTRAINT_VIOLATION | A constraint failed. | HOLD, with the violation. |
A case that cannot be run as sent (a dated rule and no decision date, a jurisdiction-scoped rule and no jurisdiction, an ordering of text, a division by zero) stops with a typed error, and the worker holds it with that error as the reason. Obligations and prohibitions are reported with the decision; they do not change it.
Errors#
A line that is not blank, a comment, an annotation or one of the rule forms stops the whole rulebook from loading, with the line number. Everything else the engine checks when it loads a rulebook is collected and reported together, each problem with the rule id and a code: forms that cannot be evaluated (see Rejected forms), duplicate rule ids, @status and @authority values outside their vocabularies, dates that do not parse, @exceptions that name no rule, required facts that a rule derives, cycles through NOT, and vocabulary the rulebook's ontology does not declare. Nothing is repaired or skipped.
# A line that is not a rule, an annotation, a comment or blank stops the whole rulebook from loading.
# case any: {"credit_score": 500}
G09A: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
G09B: WHEN HAS_SANCTIONS_HIT == 1 THEN RECOMMEND_HOLD
load failed: Rulebook ref-grammar-g09-parse-error@v1 failed to load: G09B: unsupported_rule_form: Line 5: unsupported rule form: 'WHEN HAS_SANCTIONS_HIT == 1 THEN RECOMMEND_HOLD' G09B: unsupported_rule_form no rule from this file is mounted
# Annotation values the engine acts on are checked when the rulebook loads; every problem is listed.
# case any: {"credit_score": 820}
@status archived
@authority manager
A1: IF HAS_CREDIT_SCORE >= 800 THEN PRIME_BORROWER
@effective_from 2027-02-30
A2: IF HAS_CREDIT_SCORE >= 650 THEN RECOMMEND_APPROVE
load failed: Rulebook ref-grammar-g12-annotation-errors@v1 failed to load: A1: rule_status_unknown: @status 'archived' is not one of ['active', 'draft', 'retired', 'superseded', 'suspended', 'withdrawn']; A1: authority_unknown: @authority 'manager' is not one of ['statute', 'regulation', 'policy', 'procedure', 'guidance']; A2: effective_date_invalid: @effective_from '2027-02-30' is not an ISO date A1: rule_status_unknown A1: authority_unknown A2: effective_date_invalid no rule from this file is mounted
Annotations#
The language accepts any annotation name. The names below are the ones it recognizes; any other name is kept as a custom annotation. In the table, Enforced means the engine acts on the value when a case runs, Checked means the engine checks it against the rulebook's ontology when the rulebook loads, Reported means the value is stored and shown in the worker's rulebook view, and Recorded means it is stored with the rule and nothing acts on it.
What the engine acts on
Status, effective dates, jurisdiction, exceptions and required facts decide whether a rule can fire. Authority and priority decide which recommendation wins. Ontology, predicates and classes are checked when the rulebook loads. Everything else is recorded, or reported in the rulebook view. Some recorded annotations are also read by Helixor's reasoning service when it selects rules for a reasoning problem; this page covers the rule language as a digital worker runs it.
| Annotation | Format | Today |
|---|---|---|
@domain | One word or code, for example equipment_credit | Recorded |
@ontology | The ontology id, for example example_credit_ops | Checked |
@ontology_ver | The ontology version, for example 1 | Recorded |
@authority | statute, regulation, policy, procedure or guidance | Enforced |
@owner | Free text, for example credit-policy | Reported |
@severity | Free text | Reported |
@category | Free text, for example knockout | Reported |
@priority | A number (integer or decimal) | Enforced |
@required_facts | A list of fact names in brackets, for example [HAS_CREDIT_SCORE, HAS_REQUESTED_AMOUNT] | Enforced |
@required_fact_mode | explicit | Enforced |
@english | One sentence | Reported |
@uses_predicates | A list of predicate names from the ontology, in brackets | Checked |
@uses_classes | A list of class names from the ontology, in brackets | Checked |
@param | A name and a number or code, for example score_floor 650 | Enforced |
any other @name | @name value, any name the language does not know | Recorded |
@jurisdiction | A code | Enforced |
@effective_from | An ISO date (2027-01-01) or date-time | Enforced |
@effective_to | An ISO date (2027-01-01) or date-time | Enforced |
@status | draft, active, suspended, retired, superseded or withdrawn | Enforced |
@profile_id | A code | Recorded |
@citations | A list in brackets | Reported |
@source_documents | A list in brackets | Recorded |
@references | A list of rule ids in brackets | Recorded |
@page_refs | A list in brackets | Recorded |
@paragraph_refs | A list in brackets | Recorded |
@precedent_refs | A list in brackets | Recorded |
@condition_refs | A list in brackets | Recorded |
@supersedes_rule_id_list | A list of rule ids, in brackets or separated by commas | Recorded |
@exceptions | A list of rule ids in brackets | Enforced |
@depends_on | A list of rule ids in brackets | Recorded |
@ontology_refs | A list in brackets | Recorded |
@subject_type | A class name | Recorded |
@predicate_type | A predicate name | Recorded |
@object_type | A class name | Recorded |
@origin | A word, for example direct or implied | Recorded |
@relation_confidence | A number from 0 to 1 | Recorded |
@trust_score | A number from 0 to 1 | Recorded |
@trust_tier | A word | Recorded |
@completeness | A word | Recorded |
@motif | One code | Recorded |
@motifs | A list in brackets | Recorded |
@categories | A list in brackets | Recorded |
Core annotations#
The annotations every rule in a production rulebook should carry.
@domain#
The business domain the rule belongs to.
- format
- One word or code, for example equipment_credit.
- required
- No. Without it the rule takes the rulebook's domain.
- today
- Recorded Stored as the rule's scope domain. Nothing filters rules by domain.
# report: stored
# case low-score: {"credit_score": 520}
@domain equipment_credit
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): scope.domain = "equipment_credit" metadata.domain = "equipment_credit" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@ontology#
The ontology whose vocabulary the rule uses.
- format
- The ontology id, for example example_credit_ops.
- also written
@ontology_pack_name- required
- No.
- today
- Checked Stored, and added to the rule's ontology references as
ontology_pack:<id>. A rulebook that names an ontology is bound to it: the worker must have that ontology (the rulebook can ship it), types case facts against it, and the engine rejects the rulebook if this id is not the ontology it was given or if a rule uses vocabulary the ontology does not declare. A rulebook that names an ontology the worker does not have does not load, unless its manifest sets"ontology_mode": "permissive"(see Binding rules to the ontology). The rulebook view'sontology_packfield shows the rulebook manifest's ontology.
# report: stored
# case low-score: {"credit_score": 520}
@ontology example_credit_ops
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): ontology_references = ["ontology_pack:example_credit_ops"] metadata.ontology_pack = "example_credit_ops" A1 in the worker's rulebook view: annotations.ontology_pack = "example_credit_ops" case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@ontology_ver#
The version of that ontology the rule was written against.
- format
- The ontology version, for example 1.
- also written
@pack_version- required
- No.
- today
- Recorded Stored as
ontology_versionand added to the ontology references asontology_version:<version>. The version is not compared with the ontology's.
# report: stored
# case low-score: {"credit_score": 520}
@ontology example_credit_ops
@ontology_ver 1
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): ontology_references = ["ontology_pack:example_credit_ops", "ontology_version:1"] metadata.ontology_pack = "example_credit_ops" metadata.ontology_version = "1" A1 in the worker's rulebook view: annotations.ontology_pack = "example_credit_ops" case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@authority#
Where the rule's force comes from.
- format
- One of statute, regulation, policy, procedure, guidance, highest first. Upper-cased, spaces become _. Any other value stops the rulebook from loading.
- required
- No. A rule without it ranks below guidance.
- today
- Enforced Stored as
authority_level_code(herePOLICY) and shown in the rulebook view. When recommendations conflict, the higher authority wins (see Conflicting recommendations).
# report: stored
# case low-score: {"credit_score": 520}
@authority policy
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): scope.metadata.authority_level_code = "POLICY" metadata.authority_level_code = "POLICY" metadata.authority_scope.authority_level_code = "POLICY" A1 in the worker's rulebook view: annotations.authority = "POLICY" case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@owner#
Who answers for the rule.
- format
- Free text, for example credit-policy.
- required
- No.
- today
- Reported Stored and shown in the rulebook view.
# report: stored
# case low-score: {"credit_score": 520}
@owner credit-policy
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.owner = "credit-policy" A1 in the worker's rulebook view: annotations.owner = "credit-policy" case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@severity#
How binding the rule is meant to be.
- format
- Free text; the convention is hard or soft. Not validated.
- required
- No.
- today
- Reported Stored and shown in the rulebook view. It does not rank recommendations: use
@authorityand@priorityfor that (see Severity).
# report: stored
# case low-score: {"credit_score": 520}
@severity hard
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.severity = "hard" A1 in the worker's rulebook view: annotations.severity = "hard" case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@category#
A grouping for review and reporting. For several groupings use @categories.
- format
- Free text, for example knockout.
- required
- No.
- today
- Reported Stored and shown in the rulebook view.
# report: stored
# case low-score: {"credit_score": 520}
@category knockout
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.category = "knockout" A1 in the worker's rulebook view: annotations.category = "knockout" case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@priority#
How strongly the rule's recommendation counts against others of the same authority.
- format
- A number (integer or decimal). Anything else stops the rulebook from loading.
- also written
@rule_priority,@precedence- required
- No. Without it the priority is 0.
- today
- Enforced Becomes the rule's salience, shown in the rulebook view. Between conflicting recommendations of the same authority, the higher priority wins (see Conflicting recommendations). It does not change evaluation order.
# report: stored
# case high-score: {"credit_score": 820}
@priority 80
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
A2: IF HAS_CREDIT_SCORE >= 650 THEN RECOMMEND_APPROVE
@priority 10.5
A3: IF HAS_CREDIT_SCORE >= 800 THEN PRIME_BORROWER
loaded 3 rules: A1 A2 A3 A1 stores (compared with the same rule without annotations): salience = 80.0 A1 in the worker's rulebook view: salience = 80.0 A2 stores (compared with the same rule without annotations): nothing extra A2 in the worker's rulebook view: nothing extra A3 stores (compared with the same rule without annotations): salience = 10.5 A3 in the worker's rulebook view: salience = 10.5 case high-score: credit_score=820 fired: A2 A3 derived facts: PRIME_BORROWER recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): A2 recommends APPROVE outcome: APPROVE
@required_facts#
The facts a rule cannot run without.
- format
- A list of fact names in brackets, for example [HAS_CREDIT_SCORE, HAS_REQUESTED_AMOUNT].
X,HAS_XandKNOWN_Xall name the case factx. A name that a rule derives stops the rulebook from loading. - required
- No.
- today
- Enforced A rule whose required fact is missing is blocked: it does not fire and the fact is reported missing. Without
@required_fact_mode explicit, the list is merged with the facts the rule's conditions read: every fact in a comparison and every plain fact that is not underNOT. Comparison tokens such asHAS_CREDIT_SCORE_LT_550, derived facts andKNOWN_tests are never added. The readiness check reports the same names (see Required facts).
# report: stored
# case low-score: {"credit_score": 520}
@required_facts [HAS_CREDIT_SCORE, HAS_REQUESTED_AMOUNT]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): required_fact_names = ["HAS_CREDIT_SCORE", "HAS_REQUESTED_AMOUNT"] A1 in the worker's rulebook view: annotations.required_fact_names = ["HAS_CREDIT_SCORE", "HAS_REQUESTED_AMOUNT"] case low-score: credit_score=520 fired: none derived facts: none recommendations: none not fired: A1 blocked (required facts missing: HAS_REQUESTED_AMOUNT) engine decision: NEED_DATA (missing_data): no rule recommended a decision; missing HAS_REQUESTED_AMOUNT stopped A1, which can lead to a recommendation (A1) outcome: NEED_DATA, missing requested_amount
@required_fact_mode#
Whether the required facts are only the listed ones.
- format
- explicit. Any other value, or none, means the default.
- required
- No.
- today
- Enforced With
explicit, the required facts are exactly the@required_factslist. Use it when a rule tests a fact that may be missing on purpose, for example a plain flag the case sends only when it is set.
# report: stored
# case low-score: {"credit_score": 520}
@required_fact_mode explicit
@required_facts [HAS_CREDIT_SCORE]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.required_fact_mode = "explicit" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@english#
The rule as a sentence a reviewer can read.
- format
- One sentence. Commas are kept; a
#with a space on both sides starts a comment. - required
- No.
- today
- Reported Shown in the rulebook view as the rule's sentence. If a rule has no
@english, the view shows@descriptioninstead, as ruleA2shows.
# report: stored
# case low-score: {"credit_score": 520}
@english A credit score below 550 holds the application.
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
@description Scores of 800 or more mark a prime borrower.
A2: IF HAS_CREDIT_SCORE >= 800 THEN PRIME_BORROWER
loaded 2 rules: A1 A2 A1 stores (compared with the same rule without annotations): metadata.english = "A credit score below 550 holds the application." A1 in the worker's rulebook view: english = "A credit score below 550 holds the application." A2 stores (compared with the same rule without annotations): metadata.description = "Scores of 800 or more mark a prime borrower." A2 in the worker's rulebook view: english = "Scores of 800 or more mark a prime borrower." case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@uses_predicates#
The ontology predicates the rule reads.
- format
- A list of predicate names from the ontology, in brackets.
- required
- No.
- today
- Checked Stored as a list, with one item or several, and shown in the rulebook view. In a rulebook bound to an ontology, a name the ontology does not declare as a predicate stops the rulebook from loading.
# report: stored
# case low-score: {"credit_score": 520}
@uses_predicates [hasCreditScore]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
@uses_predicates [hasCreditScore, hasRequestedAmount]
A2: IF HAS_CREDIT_SCORE >= 650 AND HAS_REQUESTED_AMOUNT <= 250000 THEN RECOMMEND_APPROVE
loaded 2 rules: A1 A2 A1 stores (compared with the same rule without annotations): metadata.uses_predicates = ["hasCreditScore"] A1 in the worker's rulebook view: annotations.uses_predicates = ["hasCreditScore"] A2 stores (compared with the same rule without annotations): metadata.uses_predicates = ["hasCreditScore", "hasRequestedAmount"] A2 in the worker's rulebook view: annotations.uses_predicates = ["hasCreditScore", "hasRequestedAmount"] case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD not fired: A2 blocked (required facts missing: HAS_REQUESTED_AMOUNT) engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@uses_classes#
The ontology classes the rule is about.
- format
- A list of class names from the ontology, in brackets.
- required
- No.
- today
- Checked Stored as a list and shown in the rulebook view. In a rulebook bound to an ontology, a name the ontology does not declare as a class stops the rulebook from loading.
# report: stored
# case low-score: {"credit_score": 520}
@uses_classes [Borrower]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
@uses_classes [Borrower, Application]
A2: IF HAS_CREDIT_SCORE >= 650 AND HAS_REQUESTED_AMOUNT <= 250000 THEN RECOMMEND_APPROVE
loaded 2 rules: A1 A2 A1 stores (compared with the same rule without annotations): metadata.uses_classes = ["Borrower"] A1 in the worker's rulebook view: annotations.uses_classes = ["Borrower"] A2 stores (compared with the same rule without annotations): metadata.uses_classes = ["Borrower", "Application"] A2 in the worker's rulebook view: annotations.uses_classes = ["Borrower", "Application"] case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD not fired: A2 blocked (required facts missing: HAS_REQUESTED_AMOUNT) engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@param#
A named value the next rule's conditions use.
- format
@param name value: an identifier that does not start with HAS_ or KNOWN_, and a number or a code. Repeat the line for more parameters.- required
- No.
- today
- Enforced Every whole-word use of the name in the rule's conditions is replaced by the value before the rule is parsed; the parameters are stored as
parameters(see Parameters).
Custom annotations#
Your own annotation, for example a review cycle or a reviewer list.
- format
- @name value, any name the language does not know.
- required
- No.
- today
- Recorded Stored with the rule under its own name. A value in brackets becomes a list, even with one item; any other value stays text, commas included, so
0.9is stored as"0.9". A condition that names a custom annotation stops the rulebook from loading: use@paramfor values that conditions use. One custom name is read by the worker: the first rule in a rulebook that has@headroom_factorsets the worker's pipeline headroom factor, which only its built-in capacity rules use; the rulebook view shows it too.
# report: stored, policy
# case low-score: {"credit_score": 520}
@headroom_factor 0.9
@review_cycle quarterly
@reviewers risk, compliance
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.headroom_factor = "0.9" metadata.review_cycle = "quarterly" metadata.reviewers = "risk, compliance" A1 in the worker's rulebook view: annotations.headroom_factor = "0.9" worker pipeline policy after loading: headroom_factor = 0.9 case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
Scope and life cycle#
These decide whether a rule can fire for a case. The case supplies its decision date as the fact decision_date and its jurisdiction as the fact jurisdiction; a rulebook with a dated or jurisdiction-scoped rule cannot run a case that lacks them (see Life cycle).
@jurisdiction#
Where the rule applies.
- format
- A code; upper-cased, spaces become _.
- required
- No.
- today
- Enforced Stored as the rule's
jurisdiction_codeand shown in the rulebook view. The rule fires only for a case whosejurisdictionfact is the same code (compared upper-cased). Codes match exactly: a rule forUSdoes not apply to a case inUS-CA. A case without a jurisdiction stops withjurisdiction_required.
# report: stored
# case california: {"credit_score": 520, "jurisdiction": "US-CA"}
# case new-york: {"credit_score": 520, "jurisdiction": "us-ny"}
# case none-given: {"credit_score": 520}
@jurisdiction us-ca
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): scope.jurisdiction_code = "US-CA" metadata.jurisdiction_code = "US-CA" metadata.authority_scope.jurisdiction_code = "US-CA" A1 in the worker's rulebook view: annotations.jurisdiction_code = "US-CA" case california: credit_score=520, jurisdiction="US-CA" fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD case new-york: credit_score=520, jurisdiction="us-ny" fired: none derived facts: none recommendations: none not fired: A1 out_of_jurisdiction (@jurisdiction US-CA) engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case none-given: credit_score=520 no rule ran outcome: HOLD reason: [local] [rule_engine] jurisdiction_required: rules A1 are scoped to a jurisdiction; the case must supply one
@effective_from#
The first day the rule applies.
- format
- An ISO date (2027-01-01) or date-time; a value that does not parse stops the rulebook from loading.
- required
- No.
- today
- Enforced Stored as
effective_from_utcand shown in the rulebook view. The rule fires only for a decision date on or after it (a date means the start of that day, UTC). A case without a decision date stops withdecision_date_required.
# report: stored
# case before: {"credit_score": 520, "decision_date": "2026-10-01"}
# case from-2027: {"credit_score": 520, "decision_date": "2027-01-01"}
# case no-date: {"credit_score": 520}
@effective_from 2027-01-01
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): scope.effective_from_utc = "2027-01-01" metadata.effective_from_utc = "2027-01-01" metadata.authority_scope.effective_from_utc = "2027-01-01" A1 in the worker's rulebook view: annotations.effective_from_utc = "2027-01-01" case before: credit_score=520, decision_date="2026-10-01" fired: none derived facts: none recommendations: none not fired: A1 not_in_effect (in effect from 2027-01-01) engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case from-2027: credit_score=520, decision_date="2027-01-01" fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD case no-date: credit_score=520 no rule ran outcome: HOLD reason: [local] [rule_engine] decision_date_required: rules A1 have an effective window; the case must supply a decision date
@effective_to#
The last day the rule applies.
- format
- An ISO date or date-time; a value that does not parse stops the rulebook from loading.
- required
- No.
- today
- Enforced Stored as
effective_to_utcand shown in the rulebook view. The rule fires only for a decision date on or before it (a date means the whole of that day, UTC).
# report: stored
# case until-end-2020: {"credit_score": 520, "decision_date": "2020-12-31"}
# case after: {"credit_score": 520, "decision_date": "2026-10-01"}
@effective_to 2020-12-31
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): scope.effective_to_utc = "2020-12-31" metadata.effective_to_utc = "2020-12-31" metadata.authority_scope.effective_to_utc = "2020-12-31" A1 in the worker's rulebook view: annotations.effective_to_utc = "2020-12-31" case until-end-2020: credit_score=520, decision_date="2020-12-31" fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD case after: credit_score=520, decision_date="2026-10-01" fired: none derived facts: none recommendations: none not fired: A1 not_in_effect (in effect until 2020-12-31) engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD
@status#
Where the rule is in its life cycle.
- format
- draft, active, suspended, retired, superseded or withdrawn; not case sensitive. Any other value stops the rulebook from loading.
- required
- No. Without it the status is draft.
- today
- Enforced Becomes the rule's status, shown in the rulebook view. draft and active rules can fire; suspended, retired, superseded and withdrawn rules never fire. Whether a rulebook of draft rules may decide real cases is the rulebook's approval, not this annotation.
# report: stored
# case low-score: {"credit_score": 520}
@status retired
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
@status active
A2: IF HAS_CREDIT_SCORE < 600 THEN WEAK_CREDIT
loaded 2 rules: A1 A2 A1 stores (compared with the same rule without annotations): status = "retired" metadata.status = "retired" A1 in the worker's rulebook view: status = "retired" A2 stores (compared with the same rule without annotations): status = "active" metadata.status = "active" A2 in the worker's rulebook view: status = "active" case low-score: credit_score=520 fired: A2 derived facts: WEAK_CREDIT recommendations: none not fired: A1 inactive (@status retired) engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD
@profile_id#
The case profile the rule is meant for.
- format
- A code.
- required
- No.
- today
- Recorded Stored as the rule's scope profile. Nothing filters rules by it.
# report: stored
# case low-score: {"credit_score": 520}
@profile_id small-business
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): scope.profile_id = "small-business" metadata.profile_id = "small-business" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
Sources and citations#
Where the rule comes from.
@citations#
The policy text the rule implements.
- format
- A list in brackets; case is kept.
- also written
@citation- required
- No.
- today
- Reported Becomes the rule's citations, shown in the rulebook view.
# report: stored
# case low-score: {"credit_score": 520}
@citations [Credit Policy 4.2, Credit Policy 4.3]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): citations = ["Credit Policy 4.2", "Credit Policy 4.3"] metadata.authority_scope.citation_ref_list = ["Credit Policy 4.2", "Credit Policy 4.3"] A1 in the worker's rulebook view: annotations.citations = ["Credit Policy 4.2", "Credit Policy 4.3"] case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@source_documents#
The documents the rule was taken from.
- format
- A list in brackets.
- also written
@source_document,@source_docs- required
- No.
- today
- Recorded Becomes the rule's source document ids.
# report: stored
# case low-score: {"credit_score": 520}
@source_documents [credit-policy-2026]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): source_document_ids = ["credit-policy-2026"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@references#
Other rules this rule refers to.
- format
- A list of rule ids in brackets.
- also written
@refs,@ref- required
- No.
- today
- Recorded Stored. The keys
@reference_rule_idsand@referenced_rule_idsare stored the same way under their own names.
# report: stored
# case low-score: {"credit_score": 520}
@refs [A2]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
A2: IF HAS_CREDIT_SCORE >= 650 THEN RECOMMEND_APPROVE
loaded 2 rules: A1 A2 A1 stores (compared with the same rule without annotations): metadata.references = ["A2"] A1 in the worker's rulebook view: nothing extra A2 stores (compared with the same rule without annotations): nothing extra A2 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@page_refs#
Pages of the source.
- format
- A list in brackets.
- also written
@pages- required
- No.
- today
- Recorded Stored.
# report: stored
# case low-score: {"credit_score": 520}
@pages [12, 13]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.page_ref_list = ["12", "13"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@paragraph_refs#
Paragraphs of the source.
- format
- A list in brackets.
- also written
@paragraphs- required
- No.
- today
- Recorded Stored.
# report: stored
# case low-score: {"credit_score": 520}
@paragraphs [4.2.1]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.paragraph_ref_list = ["4.2.1"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@precedent_refs#
Earlier decisions the rule follows.
- format
- A list in brackets.
- also written
@precedents- required
- No.
- today
- Recorded Stored.
# report: stored
# case low-score: {"credit_score": 520}
@precedents [case-2025-118]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.precedent_ref_list = ["case-2025-118"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@condition_refs#
Named conditions in the source.
- format
- A list in brackets.
- required
- No.
- today
- Recorded Stored.
# report: stored
# case low-score: {"credit_score": 520}
@condition_refs [score-floor]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.condition_ref_list = ["score-floor"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@supersedes_rule_id_list#
Rules this one replaces.
- format
- Rule ids separated by commas; upper-cased.
- required
- No.
- today
- Recorded Stored as a list. It does not stop the superseded rules: to retire a rule, give it
@status superseded.
# report: stored
# case low-score: {"credit_score": 520}
@supersedes_rule_id_list old-score-floor, old-score-floor-v0
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.supersedes_rule_id_list = ["OLD-SCORE-FLOOR", "OLD-SCORE-FLOOR-V0"] metadata.authority_scope.supersedes_rule_id_list = ["OLD-SCORE-FLOOR", "OLD-SCORE-FLOOR-V0"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
Links between rules#
Relationships between rules.
@exceptions#
Rules that are exceptions to this one.
- format
- A list of rule ids in brackets.
- also written
@exception_rules- required
- No.
- today
- Enforced Becomes the rule's exceptions.
A1with@exceptions [A2]means A2 is an exception to A1: when A2 fires, A1 is suppressed and its conclusions are not derived. The engine runs A2 first. An id that names no rule of the rulebook stops it from loading; an id of a rule that was expanded (<->,OR) covers every part.@exception_for_rule_ids [A1]on A2 says the same from the other side.
# report: stored
# case long-standing-low-score: {"credit_score": 520, "customer_years": 12}
# case new-low-score: {"credit_score": 520, "customer_years": 2}
@exceptions [A2]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
A2: IF HAS_CREDIT_SCORE < 550 AND HAS_CUSTOMER_YEARS >= 10 THEN RECOMMEND_APPROVE
loaded 2 rules: A1 A2 A1 stores (compared with the same rule without annotations): exceptions = ["A2"] A1 in the worker's rulebook view: annotations.exceptions = ["A2"] A2 stores (compared with the same rule without annotations): nothing extra A2 in the worker's rulebook view: nothing extra case long-standing-low-score: credit_score=520, customer_years=12 fired: A2 derived facts: none recommendations: RECOMMEND_APPROVE not fired: A1 suppressed (exception rule A2 fired) engine decision: APPROVE (only_candidate): A2 recommends APPROVE outcome: APPROVE case new-low-score: credit_score=520, customer_years=2 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@depends_on#
Rules this rule depends on.
- format
- A list of rule ids in brackets.
- required
- No.
- today
- Recorded Stored. The keys
@depends_on_rule_idsand@dependent_rule_idsare stored the same way. The engine orders rules by what they read and negate, not by this annotation.
# report: stored
# case low-score: {"credit_score": 520}
A0: IF HAS_CREDIT_SCORE THEN SCORE_KNOWN
@depends_on [A0]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 2 rules: A0 A1 A0 stores (compared with the same rule without annotations): nothing extra A0 in the worker's rulebook view: nothing extra A1 stores (compared with the same rule without annotations): metadata.depends_on = ["A0"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A0 A1 derived facts: SCORE_KNOWN recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
Ontology and relationship terms#
Recorded with the rule. The digital worker does not act on them; they are for rule retrieval and review.
@ontology_refs#
Further ontology terms the rule refers to.
- format
- A list in brackets.
- also written
@ontology_references- required
- No.
- today
- Recorded Added to the rule's ontology references as written.
# report: stored
# case low-score: {"credit_score": 520}
@ontology_refs [Borrower.hasCreditScore]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): ontology_references = ["Borrower.hasCreditScore"] metadata.ontology_ref_list = ["Borrower.hasCreditScore"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@subject_type#
The class at the start of the relationship the rule is about.
- format
- A class name.
- required
- No.
- today
- Recorded Stored as
relation_subject_typeand added to the ontology references.
# report: stored
# case low-score: {"credit_score": 520}
@subject_type Borrower
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): ontology_references = ["relation_subject_type:Borrower"] metadata.relation_subject_type = "Borrower" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@predicate_type#
The relationship the rule is about.
- format
- A predicate name.
- also written
@relation_type- required
- No.
- today
- Recorded Stored as
relation_predicateand added to the ontology references.
# report: stored
# case low-score: {"credit_score": 520}
@predicate_type hasCreditScore
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): ontology_references = ["relation_predicate:hasCreditScore"] metadata.relation_predicate = "hasCreditScore" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@object_type#
The class at the end of that relationship.
- format
- A class name.
- required
- No.
- today
- Recorded Stored as
relation_object_typeand added to the ontology references.
# report: stored
# case low-score: {"credit_score": 520}
@object_type CreditScore
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): ontology_references = ["relation_object_type:CreditScore"] metadata.relation_object_type = "CreditScore" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@origin#
Whether the relationship must be stated or may be inferred.
- format
- A word, for example direct or implied; lower-cased.
- required
- No.
- today
- Recorded Stored as
relation_origin. The digital worker does not use it.
# report: stored
# case low-score: {"credit_score": 520}
@origin Direct
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.relation_origin = "direct" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@relation_confidence#
The lowest confidence a relationship needs.
- format
- A number from 0 to 1.
- required
- No.
- today
- Recorded Stored as
min_relation_confidence. The digital worker does not use it.
# report: stored
# case low-score: {"credit_score": 520}
@relation_confidence 0.8
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.min_relation_confidence = 0.8 A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@trust_score#
The lowest trust score a source needs.
- format
- A number from 0 to 1.
- required
- No.
- today
- Recorded Stored as
min_trust_score. The digital worker does not use it.
# report: stored
# case low-score: {"credit_score": 520}
@trust_score 0.75
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.min_trust_score = 0.75 A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@trust_tier#
The lowest trust tier a source needs.
- format
- A word; lower-cased.
- required
- No.
- today
- Recorded Stored as
min_trust_tier. The digital worker does not use it.
# report: stored
# case low-score: {"credit_score": 520}
@trust_tier Verified
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.min_trust_tier = "verified" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@completeness#
How complete the facts must be.
- format
- A word; lower-cased.
- required
- No.
- today
- Recorded Stored as
required_completeness_status. The digital worker does not use it.
# report: stored
# case low-score: {"credit_score": 520}
@completeness Complete
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.required_completeness_status = "complete" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
Grouping#
Labels for retrieval and review.
@motif#
A pattern label for retrieval and review.
- format
- One code.
- also written
@motif_code,@helix_motif- required
- No.
- today
- Recorded Stored.
# report: stored
# case low-score: {"credit_score": 520}
@motif credit.score_floor
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.motif = "credit.score_floor" A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@motifs#
Several pattern labels.
- format
- A list in brackets.
- also written
@motif_codes,@helix_motifs- required
- No.
- today
- Recorded Stored.
# report: stored
# case low-score: {"credit_score": 520}
@motifs [credit.score_floor, credit.knockout]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.motifs = ["credit.score_floor", "credit.knockout"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
@categories#
Several groupings.
- format
- A list in brackets.
- also written
@rule_categories- required
- No.
- today
- Recorded Stored. The rulebook view shows only
@category.
# report: stored
# case low-score: {"credit_score": 520}
@categories [knockout, credit]
A1: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: A1 A1 stores (compared with the same rule without annotations): metadata.categories = ["knockout", "credit"] A1 in the worker's rulebook view: nothing extra case low-score: credit_score=520 fired: A1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): A1 recommends HOLD outcome: HOLD
Concepts#
Required facts and missing data#
Missing data is handled in two places. The decision type lists the facts it needs; if one is missing, the worker returns NEED_DATA with the names and runs no rule. Each rule also has required facts, from @required_facts and, unless @required_fact_mode explicit, the facts its conditions read. A rule whose required fact is missing is blocked when the case runs. A fact sent as null is missing in both places.
# Two layers: the decision type's required facts (checked before any rule runs)
# and each rule's required facts, enforced when rules fire: a rule whose required fact is missing is blocked.
# NEED_DATA means missing data stopped a rule that could change the decision.
# requires: credit_score, requested_amount
# report: readiness
# case complete: {"credit_score": 700, "requested_amount": 90000, "debt_service_coverage": 1.4}
# case no-coverage: {"credit_score": 700, "requested_amount": 90000}
# case no-score: {"requested_amount": 90000, "debt_service_coverage": 1.4}
# case score-null: {"credit_score": null, "requested_amount": 90000, "debt_service_coverage": 1.4}
@required_fact_mode explicit
@required_facts [HAS_DEBT_SERVICE_COVERAGE]
C01A: IF HAS_DEBT_SERVICE_COVERAGE < 1.0 THEN RECOMMEND_HOLD
@required_fact_mode explicit
@required_facts [HAS_REQUESTED_AMOUNT]
C01B: IF HAS_REQUESTED_AMOUNT > 250000 THEN LARGE_EXPOSURE
@required_facts [HAS_CREDIT_SCORE]
C01C: IF HAS_CREDIT_SCORE >= 650 AND NOT LARGE_EXPOSURE THEN RECOMMEND_APPROVE
loaded 3 rules: C01A C01B C01C
case complete: credit_score=700, requested_amount=90000, debt_service_coverage=1.4
fired: C01C
derived facts: none
recommendations: RECOMMEND_APPROVE
engine decision: APPROVE (only_candidate): C01C recommends APPROVE
outcome: APPROVE
readiness check: ready
C01A requires HAS_DEBT_SERVICE_COVERAGE; all present
C01B requires HAS_REQUESTED_AMOUNT; all present
C01C requires HAS_CREDIT_SCORE; all present
case no-coverage: credit_score=700, requested_amount=90000
fired: C01C
derived facts: none
recommendations: RECOMMEND_APPROVE
not fired: C01A blocked (required facts missing: HAS_DEBT_SERVICE_COVERAGE)
engine decision: NEED_DATA (missing_data): APPROVE from C01C is not safe: missing HAS_DEBT_SERVICE_COVERAGE stopped C01A, which could lead to an outranking recommendation
outcome: NEED_DATA, missing debt_service_coverage
readiness check: missing_facts
C01A requires HAS_DEBT_SERVICE_COVERAGE; missing HAS_DEBT_SERVICE_COVERAGE
C01B requires HAS_REQUESTED_AMOUNT; all present
C01C requires HAS_CREDIT_SCORE; all present
case no-score: requested_amount=90000, debt_service_coverage=1.4
no rule ran
outcome: NEED_DATA, missing credit_score
readiness check: missing_facts
C01A requires HAS_DEBT_SERVICE_COVERAGE; all present
C01B requires HAS_REQUESTED_AMOUNT; all present
C01C requires HAS_CREDIT_SCORE; missing HAS_CREDIT_SCORE
case score-null: credit_score=null, requested_amount=90000, debt_service_coverage=1.4
no rule ran
outcome: NEED_DATA, missing credit_score
readiness check: missing_facts
C01A requires HAS_DEBT_SERVICE_COVERAGE; all present
C01B requires HAS_REQUESTED_AMOUNT; all present
C01C requires HAS_CREDIT_SCORE; missing HAS_CREDIT_SCORE
Case no-coverage: C01C recommends approval, but C01A, which could hold the case and would win the tie, is blocked by the missing coverage, so the outcome is NEED_DATA. Case score-null sends the score as null and stops at the decision type's check, like case no-score.
Derived facts and chaining#
A conclusion becomes a fact that other rules read, so rules build on each other in any order. Derived facts are flags that carry no value; a comparison on one stops the rulebook from loading (see Rejected forms). Derived facts last for the run only.
# A conclusion becomes a fact later rules can read. Derived facts are flags: they carry no value.
# case thin-and-weak: {"debt_service_coverage": 1.1, "credit_score": 600}
# case strong: {"debt_service_coverage": 1.8, "credit_score": 720}
C02A: IF HAS_DEBT_SERVICE_COVERAGE < 1.25 THEN THIN_CASHFLOW
C02B: IF HAS_CREDIT_SCORE < 650 THEN WEAK_CREDIT
C02C: IF THIN_CASHFLOW AND WEAK_CREDIT THEN COMPOUND_RISK
C02D: IF COMPOUND_RISK THEN RECOMMEND_HOLD AND REFER_RECOMMENDED
C02F: IF HAS_CREDIT_SCORE AND NOT COMPOUND_RISK THEN RECOMMEND_APPROVE
loaded 5 rules: C02A C02B C02C C02D C02F case thin-and-weak: debt_service_coverage=1.1, credit_score=600 fired: C02A C02B C02C C02D derived facts: THIN_CASHFLOW WEAK_CREDIT COMPOUND_RISK REFER_RECOMMENDED recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): C02D recommends HOLD outcome: HOLD case strong: debt_service_coverage=1.8, credit_score=720 fired: C02F derived facts: none recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): C02F recommends APPROVE outcome: APPROVE
Severity: hard and soft#
@severity is recorded and shown in the rulebook view; it does not rank recommendations. Here the soft rule C03B and the hard rule C03C have the same authority and priority, so the tie-breaker holds the case. Give rules an @authority and a @priority to rank them.
# @severity is recorded and reported; it does not rank recommendations. Here the two rules
# have the same authority and priority, so the tie-breaker decides: hold beats approve.
# report: stored
# case young-business-good-score: {"years_in_business": 1, "credit_score": 720, "sanctions_hit": 0}
@severity hard
C03A: IF HAS_SANCTIONS_HIT == 1 THEN RECOMMEND_HOLD
@severity soft
C03B: IF HAS_YEARS_IN_BUSINESS < 2 THEN RECOMMEND_HOLD
@severity hard
C03C: IF HAS_CREDIT_SCORE >= 650 THEN RECOMMEND_APPROVE
loaded 3 rules: C03A C03B C03C C03A stores (compared with the same rule without annotations): metadata.severity = "hard" C03A in the worker's rulebook view: annotations.severity = "hard" C03B stores (compared with the same rule without annotations): metadata.severity = "soft" C03B in the worker's rulebook view: annotations.severity = "soft" C03C stores (compared with the same rule without annotations): metadata.severity = "hard" C03C in the worker's rulebook view: annotations.severity = "hard" case young-business-good-score: years_in_business=1, credit_score=720, sanctions_hit=0 fired: C03B C03C derived facts: none recommendations: RECOMMEND_HOLD RECOMMEND_APPROVE engine decision: HOLD (outcome_tie_breaker): C03B and C03C tie on authority and priority; HOLD beats APPROVE (decline > hold > approve) outcome: HOLD
Conflicting recommendations#
When rules recommend different outcomes, the highest @authority wins (statute, regulation, policy, procedure, guidance, then none), then the highest @priority, then the more conservative outcome: decline beats hold beats approve. The engine records which rules competed, which won, which were overridden and why. RECOMMEND_DECLINE decides DECLINE.
# Conflicting recommendations: higher @authority wins, then higher @priority,
# then decline beats hold beats approve. RECOMMEND_DECLINE decides DECLINE.
# case both-fire: {"customer_years": 12, "debt_service_coverage": 0.9, "credit_score": 690}
# case approve-only: {"customer_years": 12, "debt_service_coverage": 1.6, "credit_score": 690}
# case same-authority: {"customer_years": 12, "debt_service_coverage": 1.6, "credit_score": 560}
# case decline-only: {"customer_years": 1, "debt_service_coverage": 1.6, "credit_score": 480}
@authority policy
@priority 100
C04A: IF HAS_CUSTOMER_YEARS >= 5 THEN RECOMMEND_APPROVE
@authority guidance
@priority 1
C04B: IF HAS_DEBT_SERVICE_COVERAGE < 1.0 THEN RECOMMEND_HOLD
@authority policy
@priority 100
C04C: IF HAS_CREDIT_SCORE < 500 THEN RECOMMEND_DECLINE
@authority policy
@priority 50
C04D: IF HAS_CREDIT_SCORE < 600 THEN RECOMMEND_HOLD
loaded 4 rules: C04A C04B C04C C04D case both-fire: customer_years=12, debt_service_coverage=0.9, credit_score=690 fired: C04A C04B derived facts: none recommendations: RECOMMEND_APPROVE RECOMMEND_HOLD engine decision: APPROVE (authority): C04A (policy) outranks C04B (guidance): APPROVE over HOLD outcome: APPROVE case approve-only: customer_years=12, debt_service_coverage=1.6, credit_score=690 fired: C04A derived facts: none recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): C04A recommends APPROVE outcome: APPROVE case same-authority: customer_years=12, debt_service_coverage=1.6, credit_score=560 fired: C04A C04D derived facts: none recommendations: RECOMMEND_APPROVE RECOMMEND_HOLD engine decision: APPROVE (priority): same authority; C04A priority 100 beats C04D priority 50: APPROVE over HOLD outcome: APPROVE case decline-only: customer_years=1, debt_service_coverage=1.6, credit_score=480 fired: C04C C04D derived facts: none recommendations: RECOMMEND_DECLINE RECOMMEND_HOLD engine decision: DECLINE (priority): same authority; C04C priority 100 beats C04D priority 50: DECLINE over HOLD outcome: DECLINE
In case both-fire a policy rule's approval outranks a guidance rule's hold. If a guidance rule must still stop a case, give it the authority that says so, or write the precedence into the conditions with NOT.
Obligations and prohibitions#
OBLIGATION(x) concludes OBLIGATION_X and PROHIBITION(x) concludes PROHIBITION_X; PERMISSION, EXCEPTION, SAFE_HARBOR and DISCLOSURE_REQUIRED work the same way. The worker reports them with the decision; it does not carry them out, and they do not change the outcome. Another rule can read one like any derived fact (C05D). Arguments after the first are kept with the conclusion.
# OBLIGATION(x) and PROHIBITION(x) conclude OBLIGATION_X and PROHIBITION_X. They are reported, not performed.
# A later rule can read them like any derived fact. Extra arguments are kept with the conclusion.
# case large-unaudited: {"requested_amount": 400000, "audited_financials": 0, "sanctions_hit": 0}
# case sanctions-match: {"requested_amount": 90000, "audited_financials": 1, "sanctions_hit": 1}
C05A: IF HAS_REQUESTED_AMOUNT > 250000 THEN RECOMMEND_HOLD AND OBLIGATION(credit_committee_review)
C05B: IF HAS_SANCTIONS_HIT == 1 THEN RECOMMEND_HOLD AND PROHIBITION(disclose_screening_result)
C05C: IF HAS_AUDITED_FINANCIALS == 0 THEN OBLIGATION(collect_audited_financials, before_funding)
C05D: IF OBLIGATION_CREDIT_COMMITTEE_REVIEW THEN COMMITTEE_QUEUED
loaded 4 rules: C05A C05B C05C C05D case large-unaudited: requested_amount=400000, audited_financials=0, sanctions_hit=0 fired: C05A C05C C05D derived facts: COMMITTEE_QUEUED recommendations: RECOMMEND_HOLD obligations and prohibitions: OBLIGATION_CREDIT_COMMITTEE_REVIEW; OBLIGATION_COLLECT_AUDITED_FINANCIALS (arguments: COLLECT_AUDITED_FINANCIALS, BEFORE_FUNDING) engine decision: HOLD (only_candidate): C05A recommends HOLD outcome: HOLD case sanctions-match: requested_amount=90000, audited_financials=1, sanctions_hit=1 fired: C05B derived facts: none recommendations: RECOMMEND_HOLD obligations and prohibitions: PROHIBITION_DISCLOSE_SCREENING_RESULT engine decision: HOLD (only_candidate): C05B recommends HOLD outcome: HOLD
Comparisons#
Each operator, a code, and a comparison between two case facts, on two cases. No rule recommends anything, so the engine reports NO_DECISION and the worker holds the case for a person:
# Six operators, a number or an upper-case code on the right, or another case fact.
# case score-540: {"credit_score": 540, "industry_risk": "HIGH", "requested_amount": 300000, "approved_limit": 250000}
# case score-650: {"credit_score": 650, "industry_risk": "STANDARD", "requested_amount": 100000, "approved_limit": 250000}
C06A: IF HAS_CREDIT_SCORE < 550 THEN BELOW_550
C06B: IF HAS_CREDIT_SCORE <= 650 THEN UP_TO_650
C06C: IF HAS_CREDIT_SCORE > 600 THEN ABOVE_600
C06D: IF HAS_CREDIT_SCORE >= 650 THEN FROM_650
C06E: IF HAS_CREDIT_SCORE == 650 THEN EXACTLY_650
C06F: IF HAS_CREDIT_SCORE != 650 THEN OTHER_THAN_650
C06G: IF HAS_INDUSTRY_RISK == HIGH THEN HIGH_RISK_INDUSTRY
C06H: IF HAS_REQUESTED_AMOUNT > HAS_APPROVED_LIMIT THEN OVER_LIMIT
loaded 8 rules: C06A C06B C06C C06D C06E C06F C06G C06H case score-540: credit_score=540, industry_risk="HIGH", requested_amount=300000, approved_limit=250000 fired: C06A C06B C06F C06G C06H derived facts: BELOW_550 UP_TO_650 OTHER_THAN_650 HIGH_RISK_INDUSTRY OVER_LIMIT recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case score-650: credit_score=650, industry_risk="STANDARD", requested_amount=100000, approved_limit=250000 fired: C06B C06C C06D C06E derived facts: UP_TO_650 ABOVE_600 FROM_650 EXACTLY_650 recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD
NOT of a comparison#
Conditions have three values: true, false and unknown. A comparison on a missing fact is unknown, NOT of unknown is unknown, and a rule fires only when all its conditions are true. In case score-missing, C07A (explicit mode, so the score is not a required fact) is undetermined and C07B is blocked; both report the score as missing.
# A comparison on a fact the case does not have is unknown, and so is its NOT: the rule does not fire,
# and the fact is reported missing.
# case score-540: {"credit_score": 540}
# case score-700: {"credit_score": 700}
# case score-missing: {"years_in_business": 6}
@required_fact_mode explicit
C07A: IF NOT HAS_CREDIT_SCORE < 550 THEN SCORE_550_OR_MORE
C07B: IF HAS_CREDIT_SCORE != 540 THEN OTHER_THAN_540
loaded 2 rules: C07A C07B case score-540: credit_score=540 fired: none derived facts: none recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case score-700: credit_score=700 fired: C07A C07B derived facts: SCORE_550_OR_MORE OTHER_THAN_540 recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case score-missing: years_in_business=6 fired: none derived facts: none recommendations: none not fired: C07A undetermined (unknown: HAS_CREDIT_SCORE); C07B blocked (required facts missing: HAS_CREDIT_SCORE) engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD
List-valued annotations#
A value in square brackets is a list, even with one item. Commas split only a bracketed value, or the value of an annotation that is always a list (@required_facts, @uses_predicates, @citations and the other list annotations); any other value keeps its commas. A repeated list annotation is merged without duplicates; a repeated single value is replaced by the last one. A # starts a comment only at the start of a line, or with whitespace before it and after it, so #1 stays in the value.
# A value in [brackets] is a list, even with one item. Commas split only a bracketed value # (or an annotation that is always a list, such as @required_facts); other text keeps its commas. # A repeated list annotation is merged; a repeated single value is replaced. # A # starts a comment only at the start of a line or with a space on both sides, so "#1" is text. # report: stored @english Weak credit, thin cash flow @uses_predicates [hasCreditScore] @uses_classes [Borrower] C08A: IF HAS_CREDIT_SCORE < 650 THEN WEAK_CREDIT @english Weak credit and thin cash flow together are compound risk @uses_predicates [hasCreditScore, hasDebtServiceCoverage] @uses_predicates [hasRequestedAmount, hasDebtServiceCoverage] @owner credit-risk @owner credit-policy C08B: IF WEAK_CREDIT AND HAS_DEBT_SERVICE_COVERAGE < 1.25 THEN COMPOUND_RISK @english Score below #1 in the ranking # a comment @required_facts [HAS_CREDIT_SCORE] @required_facts [HAS_REQUESTED_AMOUNT] C08C: IF HAS_CREDIT_SCORE < 500 THEN RECOMMEND_HOLD
loaded 3 rules: C08A C08B C08C C08A stores (compared with the same rule without annotations): metadata.english = "Weak credit, thin cash flow" metadata.uses_predicates = ["hasCreditScore"] metadata.uses_classes = ["Borrower"] C08A in the worker's rulebook view: english = "Weak credit, thin cash flow" annotations.uses_predicates = ["hasCreditScore"] annotations.uses_classes = ["Borrower"] C08B stores (compared with the same rule without annotations): required_fact_names = ["HAS_DEBT_SERVICE_COVERAGE"] metadata.english = "Weak credit and thin cash flow together are compound risk" metadata.uses_predicates = ["hasCreditScore", "hasDebtServiceCoverage", "hasRequestedAmount"] metadata.owner = "credit-policy" C08B in the worker's rulebook view: english = "Weak credit and thin cash flow together are compound risk" annotations.owner = "credit-policy" annotations.uses_predicates = ["hasCreditScore", "hasDebtServiceCoverage", "hasRequestedAmount"] annotations.required_fact_names = ["HAS_DEBT_SERVICE_COVERAGE"] C08C stores (compared with the same rule without annotations): required_fact_names = ["HAS_CREDIT_SCORE", "HAS_REQUESTED_AMOUNT"] metadata.english = "Score below #1 in the ranking" C08C in the worker's rulebook view: english = "Score below #1 in the ranking" annotations.required_fact_names = ["HAS_CREDIT_SCORE", "HAS_REQUESTED_AMOUNT"]
Binding rules to the ontology#
A rulebook that names an ontology, with @ontology or in its manifest, is bound to it. The worker loads that ontology, from a file the rulebook ships (path_by_role.ontology in the manifest) or from the ontologies it has, and types each case's facts against it. The engine checks, when the rulebook loads, that every @uses_predicates name is a predicate of the ontology, every @uses_classes name is a class of it, and every HAS_ or KNOWN_ fact a rule reads is carried by a predicate (its fact_key) or names a class's identity fact.
# A rulebook that names an @ontology is bound to it: the worker types case facts against it,
# and the engine checks every predicate, class and HAS_/KNOWN_ fact the rules use when it loads.
# report: vocabulary
# case low-score: {"borrower_id": "B-1", "credit_score": 520}
@ontology example_credit_ops
@ontology_ver 1
@uses_predicates [hasCreditScore]
@uses_classes [Borrower]
C09A: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
loaded 1 rule: C09A rulebook ontology: example_credit_ops (bound: yes) case low-score: worker typed 1 facts against example_credit_ops case low-score: borrower_id="B-1", credit_score=520 fired: C09A derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): C09A recommends HOLD outcome: HOLD
A rule that names vocabulary the ontology does not declare stops the rulebook from loading, with every problem listed:
# A rule that names a predicate, class or fact the ontology does not declare stops the rulebook from loading.
# case rating-only: {"credit_rating": 2}
@ontology example_credit_ops
@ontology_ver 1
@uses_predicates [hasCreditScore]
@uses_classes [Borrower]
C10A: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
@ontology example_credit_ops
@uses_predicates [hasCreditRating]
@uses_classes [Borrower, Rating]
C10B: IF HAS_CREDIT_RATING < 3 THEN RECOMMEND_HOLD
load failed: Rulebook ref-concepts-c10-ontology-rejected@v1 failed to load: C10B: ontology_predicate_unknown: @uses_predicates hasCreditRating is not declared by ontology example_credit_ops; C10B: ontology_class_unknown: @uses_classes Rating is not declared by ontology example_credit_ops; C10B: ontology_fact_unknown: HAS_CREDIT_RATING reads case fact credit_rating, which no predicate of ontology example_credit_ops carries C10B: ontology_predicate_unknown C10B: ontology_class_unknown C10B: ontology_fact_unknown no rule from this file is mounted
A rulebook that names an ontology the worker does not have does not load. A rulebook whose ontology is not available yet can say so with "ontology_mode": "permissive" in its manifest: it then loads unbound, with no typing and no vocabulary check.
OR#
OR joins alternatives in a condition, and AND binds tighter: A OR B AND C is A, or B and C together. The rule becomes one rule per alternative, <id>__or1, <id>__or2 and so on, each with the same conclusions and annotations. There are no parentheses for grouping, and OR in a conclusion or in <-> is a syntax error.
# OR joins alternatives; AND binds tighter. The rule becomes one rule per alternative,
# <id>__or1, <id>__or2, ..., each with the same conclusions. OR is not allowed in a conclusion.
# case only-a: {"sanctions_hit": 1, "recent_bankruptcy": 0}
# case only-b: {"sanctions_hit": 0, "recent_bankruptcy": 1}
# case neither: {"sanctions_hit": 0, "recent_bankruptcy": 0}
C11A: IF HAS_SANCTIONS_HIT == 1 OR HAS_RECENT_BANKRUPTCY == 1 THEN RECOMMEND_HOLD
C11B: IF KNOWN_SANCTIONS_HIT AND KNOWN_RECENT_BANKRUPTCY AND NOT RECOMMEND_HOLD THEN RECOMMEND_APPROVE
loaded 3 rules: C11A__or1 C11A__or2 C11B case only-a: sanctions_hit=1, recent_bankruptcy=0 fired: C11A__or1 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): C11A__or1 recommends HOLD outcome: HOLD case only-b: sanctions_hit=0, recent_bankruptcy=1 fired: C11A__or2 derived facts: none recommendations: RECOMMEND_HOLD engine decision: HOLD (only_candidate): C11A__or2 recommends HOLD outcome: HOLD case neither: sanctions_hit=0, recent_bankruptcy=0 fired: C11B derived facts: none recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): C11B recommends APPROVE outcome: APPROVE
Arithmetic#
Either side of a comparison can compute with +, -, *, / and parentheses over case facts and numbers. A missing fact makes the comparison unknown. Dividing by zero, or computing with text, stops the case with an evaluation error, which the worker holds for a person.
# Comparisons can compute with + - * / and parentheses over case facts and numbers.
# Dividing by zero, or computing with text, stops the case with an evaluation error.
# case covered-twice: {"monthly_income": 9000, "monthly_payment": 3000}
# case not-covered: {"monthly_income": 5000, "monthly_payment": 3000}
# case zero-payment: {"monthly_income": 5000, "monthly_payment": 0}
C12A: IF HAS_MONTHLY_INCOME > HAS_MONTHLY_PAYMENT * 2 THEN COVERED_TWICE
C12B: IF HAS_MONTHLY_INCOME / HAS_MONTHLY_PAYMENT < 1.5 THEN RECOMMEND_HOLD
loaded 2 rules: C12A C12B case covered-twice: monthly_income=9000, monthly_payment=3000 fired: C12A derived facts: COVERED_TWICE recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case not-covered: monthly_income=5000, monthly_payment=3000 fired: none derived facts: none recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case zero-payment: monthly_income=5000, monthly_payment=0 no rule ran outcome: HOLD reason: [local] [rule_engine] division_by_zero: C12B: an expression divides by zero
Parameters#
@param name value gives a threshold a name. The name is replaced by the value in the next rule's conditions, and the parameter is stored with the rule.
# @param name value names a number or code for the next rule; its conditions use the name.
# report: stored
# case score-600: {"credit_score": 600}
# case score-700: {"credit_score": 700}
@param score_floor 650
C13A: IF HAS_CREDIT_SCORE < score_floor THEN BELOW_FLOOR
loaded 1 rule: C13A C13A stores (compared with the same rule without annotations): metadata.parameters.score_floor = "650" C13A in the worker's rulebook view: nothing extra case score-600: credit_score=600 fired: C13A derived facts: BELOW_FLOOR recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD case score-700: credit_score=700 fired: none derived facts: none recommendations: none engine decision: NO_DECISION (no_recommendation): no rule recommended a decision and no rule was stopped by missing data outcome: HOLD
Life cycle#
Status, effective dates, jurisdiction and exceptions together. The case supplies decision_date and jurisdiction as facts. C14A is retired, C14B and C14C are outside their effective windows, C14D is for another jurisdiction, and C14F, an exception to C14E, fires and suppresses it.
# @status, @effective_from, @effective_to, @jurisdiction and @exceptions decide whether a rule can fire.
# The case supplies its decision date and jurisdiction as the facts decision_date and jurisdiction.
# case any: {"credit_score": 520, "customer_years": 12, "decision_date": "2026-10-01", "jurisdiction": "US-CA"}
@status retired
C14A: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_DECLINE
@effective_to 2020-12-31
C14B: IF HAS_CREDIT_SCORE < 600 THEN RECOMMEND_DECLINE
@effective_from 2099-01-01
C14C: IF HAS_CUSTOMER_YEARS >= 10 THEN RELATIONSHIP_CUSTOMER
@jurisdiction us-ny
C14D: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_DECLINE
@jurisdiction us-ca
@exceptions [C14F]
C14E: IF HAS_CREDIT_SCORE < 550 THEN RECOMMEND_HOLD
C14F: IF HAS_CREDIT_SCORE < 550 AND HAS_CUSTOMER_YEARS >= 10 THEN RECOMMEND_APPROVE AND RELATIONSHIP_EXCEPTION
loaded 6 rules: C14A C14B C14C C14D C14E C14F case any: credit_score=520, customer_years=12, decision_date="2026-10-01", jurisdiction="US-CA" fired: C14F derived facts: RELATIONSHIP_EXCEPTION recommendations: RECOMMEND_APPROVE not fired: C14A inactive (@status retired); C14B not_in_effect (in effect until 2020-12-31); C14C not_in_effect (in effect from 2099-01-01); C14D out_of_jurisdiction (@jurisdiction US-NY); C14E suppressed (exception rule C14F fired) engine decision: APPROVE (only_candidate): C14F recommends APPROVE outcome: APPROVE
Constraints#
EXACTLY_ONE(a, b, ...) requires exactly one of its conditions to be true after the rules run, and AT_MOST_K(k, a, b, ...) at most k of them. A constraint is a rule of its own, with an id and no IF. A violated constraint ends the run as CONSTRAINT_VIOLATION, whatever was recommended; the worker holds the case and reports the violation.
# EXACTLY_ONE(...) and AT_MOST_K(k, ...) are checked after the rules run.
# A violated constraint ends the case as CONSTRAINT_VIOLATION, which the digital worker holds for a person.
# case one-path: {"credit_score": 700, "customer_years": 2}
# case two-paths: {"credit_score": 700, "customer_years": 12}
C15A: IF HAS_CREDIT_SCORE >= 650 THEN FAST_TRACK
C15B: IF HAS_CUSTOMER_YEARS >= 10 THEN RELATIONSHIP_TRACK
C15C: IF FAST_TRACK OR RELATIONSHIP_TRACK THEN RECOMMEND_APPROVE
C15D: EXACTLY_ONE(FAST_TRACK, RELATIONSHIP_TRACK)
C15E: AT_MOST_K(1, RECOMMEND_APPROVE, RECOMMEND_HOLD, RECOMMEND_DECLINE)
loaded 6 rules: C15A C15B C15C__or1 C15C__or2 C15D C15E case one-path: credit_score=700, customer_years=2 fired: C15A C15C__or1 derived facts: FAST_TRACK recommendations: RECOMMEND_APPROVE engine decision: APPROVE (only_candidate): C15C__or1 recommends APPROVE outcome: APPROVE case two-paths: credit_score=700, customer_years=12 fired: C15A C15B C15C__or1 C15C__or2 derived facts: FAST_TRACK RELATIONSHIP_TRACK recommendations: RECOMMEND_APPROVE not fired: C15D violated (EXACTLY_ONE holds for FAST_TRACK, RELATIONSHIP_TRACK) engine decision: CONSTRAINT_VIOLATION (constraint): constraint violated: C15D EXACTLY_ONE holds for ['FAST_TRACK', 'RELATIONSHIP_TRACK'] outcome: HOLD
Rejected forms#
These forms parse but cannot be evaluated, so the engine rejects them when it loads the rulebook, naming the rule and the form. Nothing is silently false.
| Form | Why | Write instead |
|---|---|---|
| A condition that names a custom annotation | Annotations are not values (example). | @param name value. |
THEN NOT X | A conclusion cannot take a fact back (example). | Derive a positive fact, such as SANCTIONS_CLEARED. |
| Two rules with one id | Reports, exceptions and resolution name rules by id (example). | Unique ids. |
ENTITY(...), REL(...), HAS(entity, attribute, value) | The worker sends flat case facts, so these could never hold (example). | Compare HAS_ case facts. |
= in a condition or a conclusion | A single = is not a comparison, and derived facts carry no value (example). | == in conditions; compute values outside the rules or with arithmetic in the comparison. |
| A comparison on a derived fact | Derived facts carry no value (example). | Test the derived fact as a plain condition. |
RECOMMEND_ other than APPROVE, HOLD, DECLINE; a conclusion named HAS_ or KNOWN_; a condition with arguments | Not a decision; case facts come from the case; arguments are only for deontic forms (example). | A decision plus a derived fact that says why; a derived name of your own; one fact per condition. |
A custom annotation in a condition#
# A custom annotation is not a parameter. A condition that names one stops the rulebook from loading;
# declare the value with @param instead.
# case score-600: {"credit_score": 600}
@score_floor 650
L04A: IF HAS_CREDIT_SCORE < score_floor THEN BELOW_FLOOR
load failed: Rulebook ref-limits-l04-parameters@v1 failed to load: L04A: annotation_used_as_parameter: Line 6: rule L04A compares with 'score_floor', which is the annotation @score_floor; annotations are not substituted into conditions. Declare it with '@param score_floor <value>' L04A: annotation_used_as_parameter no rule from this file is mounted
NOT in a conclusion#
# A conclusion cannot be negated. THEN NOT X stops the rulebook from loading; derive a positive fact instead.
# case cleared: {"sanctions_hit": 0}
L05A: IF HAS_SANCTIONS_HIT == 0 THEN NOT SANCTIONS_CONCERN
L05B: IF SANCTIONS_CONCERN THEN RECOMMEND_HOLD
load failed: Rulebook ref-limits-l05-not-conclusion@v1 failed to load: L05A: negated_conclusion_unsupported: THEN NOT SANCTIONS_CONCERN: a conclusion cannot be negated; derive a positive fact (for example SANCTIONS_CONCERN_CLEARED) instead L05A: negated_conclusion_unsupported no rule from this file is mounted
Duplicate rule ids#
# Two rules with the same id stop the rulebook from loading.
# case both-true: {"sanctions_hit": 1, "recent_bankruptcy": 1}
L07: IF HAS_SANCTIONS_HIT == 1 THEN SANCTIONS_STOP
L07: IF HAS_RECENT_BANKRUPTCY == 1 THEN BANKRUPTCY_STOP
load failed: Rulebook ref-limits-l07-duplicate-ids@v1 failed to load: L07: duplicate_rule_id: Rule id 'L07' is used more than once (lines 4 and 5); rule ids must be unique. L07: duplicate_rule_id no rule from this file is mounted
Entity, relation and property forms#
# Entity, relation and property forms are not executable: they stop the rulebook from loading.
# case borrower: {"borrower_id": "B-1", "industry_segment": "IND-ST", "credit_score": 700}
L09A: IF REL(borrower, operatesIn, segment) THEN SEGMENT_KNOWN
L09B: IF HAS(borrower, credit_score, 700) THEN SCORE_700
L09C: IF ENTITY(Borrower, B-1) THEN BORROWER_KNOWN
load failed: Rulebook ref-limits-l09-structured-literals@v1 failed to load: L09A: structured_form_unsupported: IF REL_BORROWER_OPERATESIN_SEGMENT: entity, relation and property forms are not executable; compare HAS_ case facts instead; L09B: structured_form_unsupported: IF HAS_BORROWER_CREDIT_SCORE_700: entity, relation and property forms are not executable; compare HAS_ case facts instead; L09C: structured_form_unsupported: IF ENTITY_BORROWER_B_1: entity, relation and property forms are not executable; compare HAS_ case facts instead L09A: structured_form_unsupported L09B: structured_form_unsupported L09C: structured_form_unsupported no rule from this file is mounted
Assignment#
# A single = is not a comparison and a conclusion cannot set a value: both stop the rulebook from loading.
# case any: {"credit_score": 700}
L10A: IF HAS_CREDIT_SCORE >= 650 THEN APPROVED_LIMIT = 50000
L10C: IF HAS_CREDIT_SCORE = 700 THEN EXACT_700
load failed: Rulebook ref-limits-l10-assignment@v1 failed to load: L10A: assignment_unsupported: THEN APPROVED_LIMIT_EQ_50000: assignment (=) in a conclusion is not supported; derived facts carry no value; L10C: assignment_in_condition: IF HAS_CREDIT_SCORE_EQ_700: '=' is not a comparison; write HAS_CREDIT_SCORE == value L10A: assignment_unsupported L10C: assignment_in_condition no rule from this file is mounted
A comparison on a derived fact#
# Derived facts are flags with no value, so a comparison on one stops the rulebook from loading.
# case thin: {"debt_service_coverage": 1.1}
L12A: IF HAS_DEBT_SERVICE_COVERAGE < 1.25 THEN THIN_CASHFLOW
L12B: IF THIN_CASHFLOW == 1 THEN NEVER_DERIVED
load failed: Rulebook ref-limits-l12-derived-comparison@v1 failed to load: L12B: comparison_on_derived_fact: IF THIN_CASHFLOW_EQ_1: THIN_CASHFLOW is derived by a rule and carries no value; test it as a plain condition L12B: comparison_on_derived_fact no rule from this file is mounted
Unknown recommendations, case-fact conclusions, arguments#
# Recommendations other than APPROVE, HOLD and DECLINE, conclusions about case facts (HAS_, KNOWN_)
# and conditions with arguments stop the rulebook from loading.
# case any: {"credit_score": 700}
L13A: IF HAS_CREDIT_SCORE >= 650 THEN RECOMMEND_REFER
L13B: IF HAS_CREDIT_SCORE >= 650 THEN HAS_GOOD_SCORE
L13C: IF SCORE_BAND(HIGH) THEN PRIME_BORROWER
load failed: Rulebook ref-limits-l13-unknown-forms@v1 failed to load: L13A: recommendation_unknown: THEN RECOMMEND_REFER: the decisions are ['RECOMMEND_APPROVE', 'RECOMMEND_DECLINE', 'RECOMMEND_HOLD']; L13B: reserved_name_concluded: THEN HAS_GOOD_SCORE: HAS_ and KNOWN_ names are case facts; a rule cannot conclude one; L13C: arguments_unsupported: IF SCORE_BAND: a condition with arguments is not executable L13A: recommendation_unknown L13B: reserved_name_concluded L13C: arguments_unsupported no rule from this file is mounted
Deliberate limits#
What the language does not do, by design today:
| Limit | What happens |
|---|---|
| No grouping with parentheses | OR splits a condition into alternatives of ANDed conditions; NOT (A OR B) is a syntax error. Write NOT A AND NOT B. |
| Derived facts are flags | A rule cannot set a value; compute values before the case reaches the rules, or in a comparison. |
| No retraction | A conclusion, once derived, holds for the run. |
| Jurisdictions match exactly | A rule for US does not apply to a case in US-CA. |
| Recorded-only annotations | @supersedes_rule_id_list, @depends_on, @profile_id, @ontology_ver and the relationship, trust and grouping annotations are stored, not acted on. |
| Missing data and NEED_DATA | The engine follows conclusions forward from a rule stopped by missing data to the recommendations it could lead to. It does not consider that the missing fact could instead have blocked a rule through NOT. |
Plain facts without HAS_ | A plain condition on a name that no rule derives and that is not a case fact token is a required fact by default, so a rule that tests a worker-computed flag that is absent when false needs @required_fact_mode explicit. |