> For the complete documentation index, see [llms.txt](https://docs.osmosworks.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.osmosworks.com/architecture/rule-engine.md).

# Rule engine

The Osmos Rule Engine evaluates dynamic conditions over customer answers. It decides whether questions and stages appear, whether documents are mandatory, and whether charges apply. Rules are stored as JSONB ASTs (Abstract Syntax Trees) directly on service blueprint and application entities.

## Where rules are used

| Field        | Attached To                       | Purpose                                      | Null Evaluation    |
| ------------ | --------------------------------- | -------------------------------------------- | ------------------ |
| `showRule`   | `ServiceStage`, `ServiceQuestion` | Decides if the stage or question is visible. | Visible by default |
| `needRule`   | `ServiceRequirement`              | Decides if the document is mandatory.        | Needed by default  |
| `chargeRule` | `ServiceCharge`                   | Decides if the line item is priced.          | Always charged     |

## AST grammar and representation

A `Rule` is a recursive Java record (`com.kleankoder.osmos.entity.Rule`) mapped to JSONB. Every rule is either a **leaf condition** or a **composite group**:

```mermaid
graph TD
    Root["Composite Group: all (AND)"]
    
    Cond1["Leaf: request.nationality IN_SET 'ECOWAS'"]
    SubGroup["Sub-Group: any (OR)"]
    Cond2["Leaf: request.express_processing == true"]
    Cond3["Leaf: party.age >= 18"]
    
    Root --> Cond1
    Root --> SubGroup
    SubGroup --> Cond2
    SubGroup --> Cond3

    classDef group fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
    classDef leaf fill:#e8f5e9,stroke:#388e3c,stroke-width:2px;
    class Root,SubGroup group;
    class Cond1,Cond2,Cond3 leaf;
```

### 1. Leaf condition

Tests a single scoped question answer against an operand:

```json
{
  "answer": "request.nationality",
  "operator": "IN_SET",
  "set": "ECOWAS"
}
```

* `answer`: Scoped key of the target question. Must be `request.<code>` or `party.<code>`.
* `operator`: An operator supported by the question's `AnswerType`.
* Operand: Exactly one of:
  * `"value"`: Single scalar value (`"NG"`, `100`, `true`, `"2026-10-11"`).
  * `"values"`: List of values (for `IN`, `NOT_IN`, `BETWEEN`, `CONTAINS_ALL`, `CONTAINS_ANY`).
  * `"set"`: Unique code of a `ServiceValueSet` (for `IN_SET`, `NOT_IN_SET`).

### 2. Composite groups

Combines nested rules with boolean logic:

* **`all` (AND)**: Evaluates to `true` if and only if **every** nested rule matches.
* **`any` (OR)**: Evaluates to `true` if **at least one** nested rule matches.
* **`not` (NOT)**: Negates the outcome of a single nested rule.

```json
{
  "all": [
    {
      "answer": "request.visa_type",
      "operator": "EQUALS",
      "value": "BUSINESS"
    },
    {
      "any": [
        {
          "answer": "request.urgent",
          "operator": "EQUALS",
          "value": true
        },
        {
          "answer": "party.citizenship",
          "operator": "NOT_IN_SET",
          "set": "EU_MEMBERS"
        }
      ]
    }
  ]
}
```

Validation strictly rejects mixing group fields (`all`/`any`/`not`) with condition fields (`answer`/`operator`).

***

## Operator catalog

Operators are validated against the target question's `AnswerType` by `RuleOperatorCatalog`:

| Answer type                                      | Supported operators                                                                                                                                                            |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `LONG_TEXT`, `EMAIL`, `PHONE`, `ADDRESS` | `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, `IN`, `NOT_IN`, `IS_ANSWERED`, `IS_NOT_ANSWERED`                                               |
| `NUMBER`                                         | `EQUALS`, `NOT_EQUALS`, `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `BETWEEN`, `NOT_BETWEEN`, `IN`, `NOT_IN`, `IS_ANSWERED`, `IS_NOT_ANSWERED` |
| `DATE`, `TIME`                                   | `EQUALS`, `NOT_EQUALS`, `BEFORE`, `AFTER`, `BETWEEN`, `NOT_BETWEEN`, `IS_ANSWERED`, `IS_NOT_ANSWERED`                                                                          |
| `BOOLEAN`, `CONFIRMATION`                        | `EQUALS`, `NOT_EQUALS`, `IS_ANSWERED`, `IS_NOT_ANSWERED`                                                                                                                       |
| `SINGLE_CHOICE`                                  | `EQUALS`, `NOT_EQUALS`, `IN`, `NOT_IN`, `IN_SET`, `NOT_IN_SET`, `IS_ANSWERED`, `IS_NOT_ANSWERED`                                                                               |
| `MULTIPLE_CHOICE`                                | `CONTAINS_ALL`, `CONTAINS_ANY`, `NOT_CONTAINS_ANY`, `IS_ANSWERED`, `IS_NOT_ANSWERED`                                                                                           |

***

## Forward-only dependency validation

Rules must be **forward-only**: a rule on a stage, question, document, or charge may **only reference questions that appear earlier** in the service flow:

```mermaid
flowchart LR
    S1Q1["Stage 1: Question A"] --> S1Q2["Stage 1: Question B (can reference A)"]
    S1Q2 --> S2Q1["Stage 2: Question C (can reference A, B)"]
    S2Q1 -.->|Rejected: Backward Reference| S1Q1
```

### Why forward-only?

1. **Prevents dependency cycles**: Eliminates paradoxes where Question A depends on Question B, and Question B depends on Question A.
2. **Linear user experience**: Customers navigate stages in order; forward-only rules guarantee that all prerequisite answers have already been answered when evaluating visibility.

`RuleValidator` inspects the ordering of stages and questions. If a rule violates ordering, the API rejects the request with precise field-pathed errors:

```json
{
  "status": "Failed",
  "message": "Validation failed",
  "errorCode": "VALIDATION_FAILED",
  "errors": [
    {
      "field": "showRule.all[1].answer",
      "message": "Rule references question 'passport_type' which appears after this element"
    }
  ]
}
```

***

## Evaluation semantics

`RuleEvaluator` is a pure function that evaluates an AST against answers:

```java
boolean matches(Rule rule, Map<String, Object> answers, Function<String, Set<String>> valueSetLookup)
```

### Fail-closed null safety

If an answer is missing, `null`, or unanswered:

* Condition operators evaluate to `false`.
* Negative operators (`NOT_EQUALS`, `NOT_IN`, `NOT_IN_SET`) also evaluate to `false` when the answer itself is unprovided.
* This ensures stale or missing answers never inadvertently trigger charges, stage skips, or document requests.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.osmosworks.com/architecture/rule-engine.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
