Skip to main content

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 ID
  • tenantId: tenant ID
  • name: rule name (required)
  • code: rule code (required, unique within tenant)
  • description: rule description
  • scopeType: scope type (enum RuleDefinition.ScopeType, such as invoice, 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: timestamps
  • createdBy / 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 nested logical/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: equal
    • ne: not equal
  • Range:
    • gt: greater than
    • gte: greater than or equal
    • lt: less than
    • lte: less than or equal
  • Set:
    • in: in set
    • not_in: not in set
  • Fuzzy:
    • like: case-sensitive, % as wildcard
    • ilike: case-insensitive
  • Between:
    • between: value is a two-element array [min, max]
  • Null checks:
    • is_null: field is null
    • is_not_null: field is not null

Three forms of value

The value field in comparison nodes supports:

  1. Literal:

    { "type": "comparison", "field": "amount", "op": "gt", "value": 1000 }
  2. Field reference:

    {
    "type": "comparison",
    "field": "invoice.amount",
    "op": "gt",
    "value": { "type": "field", "path": "policy.single_invoice_max_amount" }
    }
    • type: "field": read value from context using path, supporting dot notation.
  3. Expression:

    {
    "type": "comparison",
    "field": "amount_excl_tax",
    "op": "gt",
    "value": { "type": "expression", "expr": "policy.sensitive_amount_min * 1.1" }
    }
    • type: "expression": Spring SpEL expression; context is 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 code and validity of predicate.

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):

  • code is required and unique within tenant.
  • name and scopeType are required.
  • predicate is required and must be valid JSON, otherwise Invalid predicate is returned.

Update rule

  • PUT /metadata/rules/{id}

Notes:

  • If predicate is 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 from 1
  • pageSize or size: page size, default 20
  • sortBy: sort field, default createdAt
  • sortDirection: asc / desc, default desc

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 ruleId or ruleCode:
    • If not found, deleted, or disabled, it throws Rule not available for evaluation.
  • The rule’s predicate is parsed into a Predicate object and evaluated.

Response: RuleEvaluationResult, core fields:

  • result: overall rule result (boolean)
  • matchedPaths: list of matched paths
  • failedPaths: list of failed paths

Path semantics:

  • Logical nodes: index-based paths:
    • root: "" (empty string)
    • first level: "0", "1", ...
    • nested: "0.1", "1.2", ...
  • Comparison nodes: prefer using field as 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 code as stable and readable identifiers (for example invoice_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.