Decision Engine
Sign Up Now →Sign Up Now

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.

PreviewDigital workersRunnable examples

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 fileWhat the runner does
# case ID: {...}Runs one case. The JSON object holds the case facts.
# requires: a, bThe decision type needs facts a and b. A case without one of them gets NEED_DATA before any rule runs.
# report: storedPrints what each rule stores, compared with the same rule line without its annotations, and what the worker's rulebook view shows for it.
# report: readinessPrints the engine's readiness check for each case: the required facts of each rule and which are missing.
# report: policyPrints the worker's pipeline policy after loading.
# report: vocabularyPrints 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.

OperatorHolds 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#

SituationWhat happens
Both sides read as numbersCompared as numbers. A case value of "742" counts as a number; true, false, "true" and "false" count as 1 and 0.
TextCodes 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 rightHAS_REQUESTED_AMOUNT > HAS_APPROVED_LIMIT compares the two values.
The fact is missingThe 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 factDerived facts carry no value. A comparison on one stops the rulebook from loading; test it as a plain condition.
Missing, 0 and falseMissing (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#

  1. Required facts of the decision type

    If the case lacks a fact the decision type lists (absent, null or blank), the outcome is NEED_DATA with the missing names, and no rule runs.

  2. Which rules can fire

    A rule with an inactive @status, an effective window that does not contain the case's decision date, or a @jurisdiction other than the case's never fires. A rule whose required facts are missing is blocked.

  3. 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.

  4. Constraints

    A violated EXACTLY_ONE or AT_MOST_K ends the run as CONSTRAINT_VIOLATION (see Constraints).

  5. Resolution

    Among the rules that recommended something, the highest @authority wins, 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).

  6. 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_DATA with the facts that are missing. NEED_DATA means exactly this: missing data stopped a rule that could change the decision.

Engine outcomeMeaningDigital worker
APPROVE, HOLD, DECLINEA recommendation won.The same outcome.
NEED_DATAMissing data stopped a rule that could change the decision.NEED_DATA, with the missing case facts.
NO_DECISIONNo rule recommended anything, and no missing data could change that.HOLD: held for a person, never approved by default.
CONSTRAINT_VIOLATIONA 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.

AnnotationFormatToday
@domainOne word or code, for example equipment_creditRecorded
@ontologyThe ontology id, for example example_credit_opsChecked
@ontology_verThe ontology version, for example 1Recorded
@authoritystatute, regulation, policy, procedure or guidanceEnforced
@ownerFree text, for example credit-policyReported
@severityFree textReported
@categoryFree text, for example knockoutReported
@priorityA number (integer or decimal)Enforced
@required_factsA list of fact names in brackets, for example [HAS_CREDIT_SCORE, HAS_REQUESTED_AMOUNT]Enforced
@required_fact_modeexplicitEnforced
@englishOne sentenceReported
@uses_predicatesA list of predicate names from the ontology, in bracketsChecked
@uses_classesA list of class names from the ontology, in bracketsChecked
@paramA name and a number or code, for example score_floor 650Enforced
any other @name@name value, any name the language does not knowRecorded
@jurisdictionA codeEnforced
@effective_fromAn ISO date (2027-01-01) or date-timeEnforced
@effective_toAn ISO date (2027-01-01) or date-timeEnforced
@statusdraft, active, suspended, retired, superseded or withdrawnEnforced
@profile_idA codeRecorded
@citationsA list in bracketsReported
@source_documentsA list in bracketsRecorded
@referencesA list of rule ids in bracketsRecorded
@page_refsA list in bracketsRecorded
@paragraph_refsA list in bracketsRecorded
@precedent_refsA list in bracketsRecorded
@condition_refsA list in bracketsRecorded
@supersedes_rule_id_listA list of rule ids, in brackets or separated by commasRecorded
@exceptionsA list of rule ids in bracketsEnforced
@depends_onA list of rule ids in bracketsRecorded
@ontology_refsA list in bracketsRecorded
@subject_typeA class nameRecorded
@predicate_typeA predicate nameRecorded
@object_typeA class nameRecorded
@originA word, for example direct or impliedRecorded
@relation_confidenceA number from 0 to 1Recorded
@trust_scoreA number from 0 to 1Recorded
@trust_tierA wordRecorded
@completenessA wordRecorded
@motifOne codeRecorded
@motifsA list in bracketsRecorded
@categoriesA list in bracketsRecorded

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's ontology_pack field 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_version and added to the ontology references as ontology_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 (here POLICY) 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 @authority and @priority for 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_X and KNOWN_X all name the case fact x. 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 under NOT. Comparison tokens such as HAS_CREDIT_SCORE_LT_550, derived facts and KNOWN_ 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_facts list. 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 @description instead, as rule A2 shows.
# 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.9 is stored as "0.9". A condition that names a custom annotation stops the rulebook from loading: use @param for values that conditions use. One custom name is read by the worker: the first rule in a rulebook that has @headroom_factor sets 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_code and shown in the rulebook view. The rule fires only for a case whose jurisdiction fact is the same code (compared upper-cased). Codes match exactly: a rule for US does not apply to a case in US-CA. A case without a jurisdiction stops with jurisdiction_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_utc and 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 with decision_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_utc and 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_ids and @referenced_rule_ids are 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

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. A1 with @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_ids and @dependent_rule_ids are 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_type and 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_predicate and 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_type and 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.

FormWhyWrite instead
A condition that names a custom annotationAnnotations are not values (example).@param name value.
THEN NOT XA conclusion cannot take a fact back (example).Derive a positive fact, such as SANCTIONS_CLEARED.
Two rules with one idReports, 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 conclusionA 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 factDerived 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 argumentsNot 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:

LimitWhat happens
No grouping with parenthesesOR 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 flagsA rule cannot set a value; compute values before the case reaches the rules, or in a comparison.
No retractionA conclusion, once derived, holds for the run.
Jurisdictions match exactlyA 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_DATAThe 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.

Next steps#