Rule Engine
Gateway prefix: ${API_BASE}/metadata/rules/...
The rule engine allows you to configure explainable, replayable business rules at the metadata layer. It separates complex decision logic from code, describes rules via JSON predicate trees, and provides an online dry-run interface.
Backend components:
- Rule management:
RuleDefinitionController(/api/v1/rules) - Rule service:
RuleDefinitionService - Execution engine:
InMemoryRuleEvaluator
RuleDefinition structure
DTO: RuleDefinitionDto.
Core fields:
id: rule IDtenantId: tenant IDname: rule name (required)code: rule code (required, unique within tenant)description: rule descriptionscopeType: scope type (enumRuleDefinition.ScopeType, such asinvoice,expense, etc. as agreed in business)scopeKey: scope key (optional, for further restricting applicability)predicate: rule predicate tree (JSON, see next section)enabled: whether enabled (controls if rule can be evaluated)createdAt/updatedAt: timestampscreatedBy/updatedBy: audit fields
The rule engine itself only handles “condition evaluation + matched path”. Scoring, risk levels, and actions are handled by upper-level business logic.
Predicate model
Predicates are represented by com.aidaas.common.predicate.Predicate and support three node types:
- Logical node (
type = "logical") - Not node (
type = "not") - Comparison node (
type = "comparison", default)
Logical node (LogicalPredicate)
Structure:
{
"type": "logical",
"op": "and",
"conditions": [
{ "type": "comparison", "field": "amount", "op": "gt", "value": 1000 },
{ "type": "comparison", "field": "status", "op": "eq", "value": "PAID" }
]
}
Fields:
type: must be"logical"op: logical operator, values:"and": passes when all child conditions are true"or": passes when any child condition is true
conditions: array of child predicates (can be nestedlogical/not/comparison)
Empty conditions:
- For
and: treated as true - For
or: treated as false
Not node (NotPredicate)
Structure:
{
"type": "not",
"op": "not",
"condition": {
"type": "comparison",
"field": "currency",
"op": "eq",
"value": "CNY"
}
}
Fields:
type: must be"not"op: must be"not"(aligned with data service side)condition: predicate to negate
Comparison node (ComparisonPredicate)
Structure:
{
"type": "comparison",
"field": "amount",
"op": "gte",
"value": 1000
}
Fields:
type:"comparison"or omitted (defaults to comparison)field: field path, dot-separated (such as"invoice.amount"or"amount")op: comparison operator (see below)value: right-hand value, supports various shapes (literal/list/special object)
Comparison operators (ComparisonOperator):
- Equality:
eq: equalne: not equal
- Range:
gt: greater thangte: greater than or equallt: less thanlte: less than or equal
- Set:
in: in setnot_in: not in set
- Fuzzy:
like: case-sensitive,%as wildcardilike: case-insensitive
- Between:
between: value is a two-element array[min, max]
- Null checks:
is_null: field is nullis_not_null: field is not null
Three forms of value
The value field in comparison nodes supports:
-
Literal:
{ "type": "comparison", "field": "amount", "op": "gt", "value": 1000 } -
Field reference:
{
"type": "comparison",
"field": "invoice.amount",
"op": "gt",
"value": { "type": "field", "path": "policy.single_invoice_max_amount" }
}type: "field": read value from context usingpath, supporting dot notation.
-
Expression:
{
"type": "comparison",
"field": "amount_excl_tax",
"op": "gt",
"value": { "type": "expression", "expr": "policy.sensitive_amount_min * 1.1" }
}type: "expression": Spring SpEL expression;contextis used as root map, and keys are also injected as variables.- If the expression throws an exception, it evaluates to null and the comparison is treated as null.
Rule management APIs
Base path: /metadata/rules (gateway to /api/v1/rules).
Permissions: tenant:admin / admin.
Headers:
Authorization: Bearer <token>X-Tenant-Id: <tenantId>
Batch create rules
- POST
/metadata/rules/batch
Request body: RuleDefinitionDto[].
Notes:
- The server creates rules per tenant, validating uniqueness of
codeand validity ofpredicate.
Create single rule
- POST
/metadata/rules
Example:
curl -X POST "${API_BASE}/metadata/rules" \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Id: tenant-abc123" \
-H "Content-Type: application/json" \
-d '{
"code": "invoice_high_amount",
"name": "High invoice amount alert",
"description": "Alert when single invoice amount exceeds 10k",
"scopeType": "invoice",
"scopeKey": null,
"predicate": {
"type": "comparison",
"field": "invoice.amount",
"op": "gt",
"value": 10000
},
"enabled": true
}'
Validation (see RuleDefinitionService.createRule):
codeis required and unique within tenant.nameandscopeTypeare required.predicateis required and must be valid JSON, otherwiseInvalid predicateis returned.
Update rule
- PUT
/metadata/rules/{id}
Notes:
- If
predicateis present in the request body, its validity is re-validated. - Other fields are updated according to the DTO.
Delete rule
- DELETE
/metadata/rules/{id}
Notes:
- Soft delete (
deleted = true); deleted rules are no longer returned or executed.
Get rule by ID
- GET
/metadata/rules/{id}
Returns a single RuleDefinitionDto, or an error if not found.
Paginated rule query
- GET
/metadata/rules?page=1&pageSize=20&sortBy=createdAt&sortDirection=desc
Query parameters:
page: page number, starting from1pageSizeorsize: page size, default20sortBy: sort field, defaultcreatedAtsortDirection:asc/desc, defaultdesc
Response: PageResponse<RuleDefinitionDto>.
Rule evaluation (dry run)
Evaluation endpoint:
- POST
/metadata/rules/evaluate
Request body: RuleEvaluationRequest, core fields:
ruleId: rule ID (one of ruleId/ruleCode)ruleCode: rule code (one of ruleId/ruleCode)context:Map<String,Object>providing field values and expression variables
Example:
curl -X POST "${API_BASE}/metadata/rules/evaluate" \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Id: tenant-abc123" \
-H "Content-Type: application/json" \
-d '{
"ruleCode": "invoice_high_amount",
"context": {
"invoice": { "amount": 12000, "currency": "CNY" },
"policy": { "single_invoice_max_amount": 10000 }
}
}'
Behavior (see RuleDefinitionService.evaluateRule and InMemoryRuleEvaluator):
- The server loads rules by
ruleIdorruleCode:- If not found, deleted, or disabled, it throws
Rule not available for evaluation.
- If not found, deleted, or disabled, it throws
- The rule’s
predicateis parsed into aPredicateobject and evaluated.
Response: RuleEvaluationResult, core fields:
result: overall rule result (boolean)matchedPaths: list of matched pathsfailedPaths: list of failed paths
Path semantics:
- Logical nodes: index-based paths:
- root:
""(empty string) - first level:
"0","1", ... - nested:
"0.1","1.2", ...
- root:
- Comparison nodes: prefer using
fieldas path (such as"invoice.amount") for easier debugging.
Frontend rule editor and dry-run (reference)
The frontend includes a rule editor and dry-run tool (/ui/web/src/ruleengine), with types in ruleTypes.ts:
- Supports visual selection by entity/field (e.g.
fin_invoice.amount) - Provides operator selection, value input (literal/expression/field reference)
- Offers preset expressions (such as time windows, behavioral metrics)
The UI generates JSON compatible with the Predicate model and sends it to the rule management and evaluation APIs.
Usage guidelines
- Design rule
codeas stable and readable identifiers (for exampleinvoice_high_amount,expense_policy_violation) for logging and audit. - Recommended pattern for business event flows:
- Start with “rule dry-run” mode: record matches and scores without blocking flows.
- After stabilization, wire in actual blocking/alerting logic.
- For frequently changing or high-risk rules, consider combining with workflows (Linker) to build “rule release approval flows” and “periodic recomputation/sampling flows” to keep governance under control.