Chapter 22 · Volume III — The Intelligence Layer

Declarative Rules Engine

22.1 Rules vs Validation vs Guards

ADL has three data integrity mechanisms. Each operates at a different scope:

FeatureScopeTimingQuestion It Answers
Validation (§14)Single record, single/multi-fieldBefore write”Are these field values valid?”
State machine guards (§12.3)Single recordBefore transition”Can this user perform this transition?”
RulesCross-resourceBefore write”Does this operation violate a business policy?”

Validation checks data shape — required fields, patterns, value ranges, date ordering. Guards check transition eligibility — role-based access and field presence. Rules check business policies that span multiple records, multiple resources, and aggregate constraints that no single record can express.

Rules evaluate BEFORE the write commits. If a reject rule fires, the write never reaches the database. This is critical — a 100% effort cap must be enforced before the commitment is recorded, not after.


22.2 Declaration

Rules are declared at the domain level because they span resources. Each rule is a named entry in the rules: block:

domain:
  name: awards
  version: "2.0.0"

  rules:
    effortCapEnforcement:
      description: "Total effort across all grants cannot exceed 100%"
      trigger:
        resource: EffortCommitment
        on: [create, update]
      condition: >
        sum(EffortCommitment, 'committedPercent',
            personnelId == $record.personnelId
            && id != $record.id
            && status != 'certified')
        + $record.committedPercent > 100
      action: reject
      message: "Total committed effort for {personnelName} would be {totalEffort}%, exceeding 100%"
      messageData:
        totalEffort: >
          sum(EffortCommitment, 'committedPercent',
              personnelId == $record.personnelId
              && id != $record.id
              && status != 'certified')
          + $record.committedPercent
      severity: error
      regulation: "2 CFR §200.430 — Compensation"

Rule Properties

PropertyTypeRequiredDefaultDescription
descriptionstringyesHuman-readable description of the business rule
triggerobjectyesWhen to evaluate the rule
conditionstringyesAEL expression — the rule fires when this evaluates to true
actionstringyesreject, warn, or log
messagestringyesHuman-readable message with {field} interpolation
messageDataobjectno{}Named computed values for message interpolation
severitystringyeserror (reject), warning (warn), info (log)
regulationstringno""Regulatory citation for compliance documentation
enabledbooleannotrueCan be disabled at runtime via the rules API
priorityintegerno100Evaluation order — lower numbers evaluate first
dependsOnstringno""Skip this rule if the named prerequisite rule fired
groupstringno""Rule group name for organizational clarity and bulk operations
notificationobjectnoFire a notification when the rule triggers (§23)
historyobjectnoRecord a history event when the rule triggers (§21)

22.3 Rule Actions

Reject — Hard Rule

action: reject
severity: error

The operation is blocked. The API returns HTTP 422:

{
  "status": "error",
  "code": 422,
  "error_code": "RULE_VIOLATION",
  "error_message": "Total committed effort for Dr. Smith would be 115%, exceeding 100%",
  "rule": "effortCapEnforcement",
  "regulation": "2 CFR §200.430 — Compensation",
  "severity": "error"
}

The record is not created, updated, or transitioned. The operation fails atomically — no partial writes. Reject is for hard constraints that must never be violated: effort caps, enrollment limits, budget ceilings, safety gates.

Warn — Soft Rule

action: warn
severity: warning

The operation proceeds, but the response includes a warning:

{
  "status": "ok",
  "code": 201,
  "data": { ... },
  "warnings": [
    {
      "rule": "procurementThresholdWarning",
      "message": "Expenditure of $32,500.00 exceeds $25,000. Competitive bidding documentation required.",
      "regulation": "2 CFR §200.320 — Procurement",
      "severity": "warning"
    }
  ]
}

The record is written. The warning is returned in the response body. If a notification is configured on the rule, it fires. If a history event is configured, it is recorded. Warn is for policies that require awareness but not prevention: threshold crossings, documentation requirements, review triggers.

Log — Informational Rule

action: log
severity: info

The operation proceeds silently. The rule evaluation result is recorded in the rules log table but no warning is returned in the API response. Log is for monitoring and analytics: large transaction flagging, pattern detection, compliance audit trails.


22.4 Trigger Configuration

The trigger object defines when a rule is evaluated:

trigger:
  resource: Expenditure
  on: [create, update]
  fields: [amount, vendor]
  condition: "$record.status == 'pending'"
PropertyTypeRequiredDefaultDescription
resourcestringyesThe resource that triggers rule evaluation
onstring or string[]yesOperations: create, update, transition, delete
fieldsstring[]noallFor update: evaluate only when these fields change
transitionstringnoFor transition: specific transition name
conditionstringno""Pre-filter AEL expression — skip evaluation entirely if false

Pre-Filter Condition

The trigger.condition is a fast single-record check that prevents the expensive cross-resource condition from evaluating when the record obviously does not apply:

trigger:
  resource: Participant
  on: [create]
  condition: "$record.status == 'enrolled' || $record.status == 'screening'"

If the participant’s status is neither enrolled nor screening, the main condition (which queries a count across all participants) is never evaluated.

Field-Scoped Triggers

For on: update, the fields property ensures the rule only evaluates when specific fields change:

trigger:
  resource: EffortCommitment
  on: [create, update]
  fields: [committedPercent, personnelId]

An effort cap rule does not need to re-evaluate when someone updates a comment field.


22.5 Condition Language

Rule conditions use the full AEL expression engine with cross-resource query functions that are not available in validation rules.

Aggregation Functions

sum(Resource, 'field', filter)     — sum field values across records
count(Resource, filter)            — count matching records
avg(Resource, 'field', filter)     — average field value
min(Resource, 'field', filter)     — minimum field value
max(Resource, 'field', filter)     — maximum field value
exists(Resource, filter)           — true if any record matches

Lookup Functions

findOne(Resource, filter)          — return single matching record
findOne(Resource, filter).field    — return a specific field from match
findMany(Resource, filter)         — return all matching records

Record Context

$record                — the record being created/updated/transitioned
$record.field          — a specific field on the record
$previous.field        — field value before the update (update only)
$transition            — transition name (transition only)

Temporal Functions

now()                          — current timestamp
daysBetween(date1, date2)      — days between two dates
daysUntil(date)                — days from now to date
yearsBetween(date1, date2)     — years between two dates

Condition Examples

Aggregate constraint — effort cap:

condition: >
  sum(EffortCommitment, 'committedPercent',
      personnelId == $record.personnelId
      && id != $record.id
      && status != 'certified')
  + $record.committedPercent > 100

Cross-resource lookup — enrollment cap:

condition: >
  count(Participant,
        studyId == $record.studyId
        && (status == 'enrolled' || status == 'screening'))
  >= findOne(Study, id == $record.studyId).targetSampleSize

Existence check — conflict of interest:

condition: >
  exists(ConflictOfInterest,
         personnelId == $record.traderId
         && entity == $record.venue
         && status == 'active')

Temporal constraint — inspection recency:

condition: >
  count(Inspection,
        assetId == $record.id
        && status == 'approved'
        && daysBetween(performedDate, now()) <= 90)
  == 0

22.6 Message Interpolation

Rule messages support {fieldName} placeholders resolved from the record and from messageData.

messageData

The messageData object defines named values computed at evaluation time and available in the message template:

message: "Total committed effort for {personnelName} would be {totalEffort}%, exceeding 100%"
messageData:
  totalEffort: >
    sum(EffortCommitment, 'committedPercent',
        personnelId == $record.personnelId
        && id != $record.id)
    + $record.committedPercent

{personnelName} resolves from $record.personnelName. {totalEffort} resolves from the messageData computation. This separation keeps the condition (a boolean test) clean while providing rich context in the message.

Regulatory Citations

The regulation field provides a machine-readable citation that appears in rejection responses and the rules log:

regulation: "2 CFR §200.430 — Compensation"

When an auditor asks “what controls prevent over-commitment of effort?” the answer is in the YAML — the policy, the enforcement, and the regulatory citation are co-located in the rule definition.


22.7 Evaluation Order

When multiple rules apply to the same operation on the same resource:

1. Rules sorted by priority (lower first, default 100)
2. All reject rules evaluated first
3. If any reject rule fires → operation blocked, no further rules evaluated
4. All warn rules evaluated → warnings collected
5. All log rules evaluated → log entries recorded

This ordering ensures hard rules are checked before soft rules, a single rejection is sufficient to block, all applicable warnings are collected together, and log rules never affect the operation outcome.

Rule Dependencies

A rule with dependsOn is skipped if the named prerequisite rule fired:

subawardOverrunCheck:
  dependsOn: subawardBudgetConsistency

If subawardBudgetConsistency rejected the operation, subawardOverrunCheck is skipped — preventing redundant messages about budget details when the operation was already rejected for a more fundamental violation.


22.8 Execution Position

Rules sit at position 6 in the write pipeline — after all single-record checks pass but before the write commits:

1. Authentication
2. Permission check
3. Field-level permission check
4. Field-level + cross-field validation
5. State machine guard check (if transition)
6. RULES EVALUATION ← reject / warn / log
7. Write to database
8. Computed field recalculation
9. Version / audit recording
10. History event emission
11. Notification dispatch
12. Workflow trigger evaluation

This position means rules only evaluate for operations that are otherwise valid, rules can reference the complete $record with merged request data, reject rules prevent the write atomically, and history events and notifications fire AFTER rules pass.


22.9 Rules Log

Every rule evaluation that fires is recorded in the {domain}_rules_log table:

ColumnTypeDescription
iduuidLog entry identifier
ruleNamestringRule definition name
actionstringreject, warn, or log
firedbooleanWhether the condition evaluated to true
messagestringInterpolated message
regulationstringRegulatory citation
resourceNamestringTriggering resource
recordIduuidTriggering record
actorIduuidUser who performed the operation
operationstringcreate, update, transition, delete
timestampdatetimeWhen the evaluation occurred

The rules log is the compliance evidence trail. When an auditor asks “how many times was the effort cap triggered?” the answer is a single query against the log table.

Verbose Logging

By default, only triggered rules (condition = true) are logged. Verbose logging records every evaluation including non-fires:

domain:
  rulesConfig:
    logAllEvaluations: false
    logRetentionDays: 365

22.10 Rules API

List Rules

GET /api/v1/{domain}/rules

Returns all rule definitions with fire counts and last-fired timestamps.

Get Rule

GET /api/v1/{domain}/rules/{name}

Returns one rule definition with statistics.

Rule Log

GET /api/v1/{domain}/rules/{name}/log?fired=true&since=2026-01-01
GET /api/v1/{domain}/rules/log?action=reject&fired=true

Query the evaluation log for a specific rule or across all rules.

Test Rule (Dry Run)

POST /api/v1/{domain}/rules/{name}/test
{ "record": { "personnelId": "p-1", "committedPercent": 60 } }

Evaluates the rule against a test record without writing anything. Returns whether the rule would fire and the interpolated message. Admin-only.

Enable / Disable

POST /api/v1/{domain}/rules/{name}/enable
POST /api/v1/{domain}/rules/{name}/disable

Toggle a rule at runtime without redeploying the domain. The state is persisted and survives restarts.

Statistics

GET /api/v1/{domain}/rules/stats

Returns total rules, enabled count, breakdown by action, last-30-day evaluation and fire counts, and top-firing rules.


22.11 Performance Mitigations

Cross-resource queries can be expensive. The rules engine uses several mechanisms:

Trigger pre-filtering. trigger.condition skips the cross-resource condition when the record does not apply.

Field-scoped triggers. fields prevents re-evaluation on irrelevant field changes.

Query caching. Within a single operation’s rule evaluation cycle, identical aggregation queries execute once. Three rules querying sum(Expenditure, 'amount', ...) with the same filter share one result.

Evaluation ordering. Reject rules evaluate first. If a reject fires, warn and log rules are skipped.

Rule dependencies. dependsOn skips redundant evaluations.

Index awareness. Fields used in rule conditions SHOULD be indexed (§19).


22.12 Rule Groups

Related rules can be grouped for bulk operations:

rules:
  _groups:
    federalCompliance:
      description: "Rules enforcing 2 CFR Part 200 requirements"
      rules: [effortCapEnforcement, procurementThresholdWarning, subawardBudgetConsistency]
      enabled: true

    exportControl:
      description: "ITAR and EAR compliance rules"
      rules: [itarPersonnelVerification, conflictOfInterestCheck]
      enabled: true

Group API

POST /api/v1/{domain}/rules/groups/{groupName}/enable
POST /api/v1/{domain}/rules/groups/{groupName}/disable

Bulk enable or disable all rules in a group. Useful for environment-specific rule sets.


End of Chapter 22.

Next: Chapter 23 — Declarative Notifications