10 KiB
Test Rule Engine Documentation
Overview
The CLQMS Rule Engine evaluates business rules that inspect orders, patients, and tests, then executes actions when the compiled condition matches.
Rules are authored using a domain specific language stored in ruledef.ConditionExpr. Before the platform executes any rule, the DSL must be compiled into JSON and stored in ConditionExprCompiled, and each rule must be linked to the tests it should influence via testrule.
Execution Flow
- Write or edit the DSL in
ConditionExpr. - POST the expression to
POST /api/rule/compileto validate syntax and produce compiled JSON. - Save the compiled payload into
ConditionExprCompiledand persist the rule inruledef. - Link the rule to one or more tests through
testrule.TestSiteID(rules only run for linked tests). - When the configured event fires (
test_createdorresult_updated), the engine evaluatesConditionExprCompiledand runs the resultingthenorelseactions.
Note: The rule engine currently fires only for
test_createdandresult_updated. Other event codes can exist in the database but are not triggered by the application unless additionalRuleEngineService::run(...)calls are added.
Event Triggers
| Event Code | Status | Trigger Point |
|---|---|---|
test_created |
Active | Fired after a new test row is persisted; the handler calls RuleEngineService::run('test_created', ...) to evaluate test-scoped rules |
result_updated |
Active | Fired whenever a test result is saved or updated so result-dependent rules run immediately |
Other event codes remain in the database for future workflows, but only test_created and result_updated are executed by the current application flow.
Rule Structure
Rule
├── Event Trigger (when to run)
├── Conditions (when to match)
└── Actions (what to do)
The DSL expression lives in ConditionExpr. The compile endpoint (/api/rule/compile) renders the lifeblood of execution, producing conditionExpr, valueExpr, then, and else nodes that the engine consumes at runtime.
Syntax Guide
Basic Format
if(condition; then-action; else-action)
Logical Operators
- Use
&&for AND (all sub-conditions must match). - Use
||for OR (any matching branch satisfies the rule). - Surround mixed logic with parentheses for clarity and precedence.
Multi-Action Syntax
Actions within any branch are separated by : and evaluated in order. Every then and else branch must end with an action; use nothing when no further work is required.
if(sex('M'); result_set(0.5):test_insert('HBA1C'); nothing)
Multiple Rules
Create each rule as its own ruledef row; do not chain expressions with commas. The testrule table manages rule-to-test mappings, so multiple rules can attach to the same test. Example:
- Insert
RULE_MALE_RESULTandRULE_SENIOR_COMMENTinruledef. - Add two
testrulerows linking each rule to the appropriateTestSiteID.
Each rule compiles and runs independently when its trigger fires and the test is linked.
Available Functions
Conditions
| Function | Description | Example |
|---|---|---|
| `sex('M' | 'F')` | Match patient sex |
| `priority('R' | 'S' | 'U')` |
age > 18 |
Numeric age comparisons (>, <, >=, <=) |
age >= 18 && age <= 65 |
requested('CODE') |
Check whether the order already requested a test (queries patres) |
requested('GLU') |
Logical Operators
| Operator | Meaning | Example |
|---|---|---|
&& |
AND (all truthy) | sex('M') && age > 40 |
| ` | ` | |
() |
Group expressions | `(sex('M') && age > 40) |
Actions
| Action | Description | Example |
|---|---|---|
result_set(value) |
Write to patres.Result for the current order/test using the provided value |
result_set(0.5) |
test_insert('CODE') |
Insert a test row by TestSiteCode if it doesn’t already exist for the order |
test_insert('HBA1C') |
test_delete('CODE') |
Remove a previously requested test from the current order when the rule deems it unnecessary | test_delete('INS') |
comment_insert('text') |
Insert an order comment (ordercom) describing priority or clinical guidance |
comment_insert('Male patient - review') |
nothing |
Explicit no-op to terminate an action chain | nothing |
Note:
set_priority()was removed. Usecomment_insert()for priority notes without altering billing.
Runtime Requirements
- Compiled expression required: Rules without
ConditionExprCompiledare ignored (seeRuleEngineService::run). - Order context:
context.order.InternalOIDmust exist for any action that writes topatresorordercom. - TestSiteID:
result_set()needstestSiteID(either provided in context or fromorder.TestSiteID).test_insert()resolves aTestSiteIDvia theTestSiteCodeinTestDefSiteModel, andtest_delete()removes the matchingTestSiteIDrows when needed. - Requested check:
requested('CODE')inspectspatresrows for the sameOrderIDandTestSiteCode.
Examples
if(sex('M'); result_set(0.5); result_set(0.6))
Returns 0.5 for males, 0.6 otherwise.
if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing)
Adds new tests when glucose is already requested.
if(sex('M') && age > 40; result_set(1.2); result_set(1.0))
if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0))
if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL'))
if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing)
if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL'))
if(requested('GLU'); test_delete('INS'):comment_insert('Duplicate insulin request removed'); nothing)
API Usage
Compile DSL
Validates the DSL and returns a compiled JSON structure that should be persisted in ConditionExprCompiled.
POST /api/rule/compile
Content-Type: application/json
{
"expr": "if(sex('M'); result_set(0.5); result_set(0.6))"
}
The response contains raw, compiled, and conditionExprCompiled fields; store the JSON payload in ConditionExprCompiled before saving the rule.
Evaluate Expression (Validation)
This endpoint simply evaluates an expression against a runtime context. It does not compile DSL or persist the result.
POST /api/rule/validate
Content-Type: application/json
{
"expr": "order[\"Age\"] > 18",
"context": {
"order": {
"Age": 25
}
}
}
Create Rule (example)
POST /api/rule
Content-Type: application/json
{
"RuleCode": "RULE_001",
"RuleName": "Sex-based result",
"EventCode": "test_created",
"ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))",
"ConditionExprCompiled": "<compiled JSON here>",
"TestSiteIDs": [1, 2]
}
Database Schema
Tables
- ruledef – stores rule metadata, raw DSL, and compiled JSON.
- testrule – mapping table that links rules to tests via
TestSiteID. - ruleaction – deprecated. Actions are now embedded in
ConditionExprCompiled.
Key Columns
| Column | Table | Description |
|---|---|---|
EventCode |
ruledef | The trigger event (typically test_created or result_updated). |
ConditionExpr |
ruledef | Raw DSL expression (semicolon syntax). |
ConditionExprCompiled |
ruledef | JSON payload consumed at runtime (then, else, etc.). |
ActionType / ActionParams |
ruleaction | Deprecated; actions live in compiled JSON now. |
Best Practices
- Always run
POST /api/rule/compilebefore persisting a rule soConditionExprCompiledexists. - Link each rule to the relevant tests via
testrule.TestSiteID—rules are scoped to linked tests. - Use multi-action (
:) to bundle several actions in a single branch; finish the branch withnothingif no further work is needed. - Prefer
comment_insert()over the removedset_priority()action when documenting priority decisions. - Group complex boolean logic with parentheses for clarity when mixing
&&and||. - Use
requested('CODE')responsibly; it performs a database lookup onpatresso avoid invoking it in high-frequency loops without reason.
Migration Guide
Syntax Changes (v2.0)
The DSL moved from ternary (condition ? action : action) to semicolon syntax. Existing rules must be migrated via the provided script.
| Old Syntax | New Syntax |
|---|---|
if(condition ? action : action) |
if(condition; action; action) |
Migration Examples
# BEFORE
if(sex('M') ? result_set(0.5) : result_set(0.6))
# AFTER
if(sex('M'); result_set(0.5); result_set(0.6))
# BEFORE
if(sex('F') ? set_priority('S') : nothing)
# AFTER
if(sex('F'); comment_insert('Female patient - review priority'); nothing)
Migration Process
Run the migration which:
- Converts ternary syntax to semicolon syntax.
- Recompiles every expression into JSON so the engine consumes
ConditionExprCompileddirectly. - Eliminates reliance on the
ruleactiontable.
php spark migrate
Troubleshooting
Rule Not Executing
- Ensure the rule has a compiled payload (
ConditionExprCompiled). - Confirm the rule is linked to the relevant
TestSiteIDintestrule. - Verify the
EventCodematches the currently triggered event (test_createdorresult_updated). - Check that
EndDate IS NULLfor bothruledefandtestrule(soft deletes disable execution). - Use
/api/rule/compileto validate the DSL and view errors.
Invalid Expression
- POST the expression to
/api/rule/compileto get a detailed compilation error. - If using
/api/rule/validate, supply the expectedcontextpayload; the endpoint simply evaluates the expression without saving it.
Runtime Errors
RESULT_SET requires context.order.InternalOIDortestSiteID: include those fields in the context passed toRuleEngineService::run().TEST_INSERTfailures mean the providedTestSiteCodedoes not exist or the rule attempted to insert a duplicate test; checktestdefsiteand existingpatresrows.COMMENT_INSERT requires comment: ensure the action provides text.