# How Securo's Rule Engine Works for Auto‑Categorization: A Complete Technical Guide

> Discover how Securo's rule engine auto-categorizes transactions. Learn about its Python evaluator, rule definition, condition matching, and action execution.

- Repository: [securo-finance/securo](https://github.com/securo-finance/securo)
- Tags: deep-dive
- Published: 2026-08-28

---

**Securo's auto‑categorization engine is a pure‑Python rule evaluator that processes transactions through three stages: JSON rule definition, condition matching with accent‑insensitive text normalization, and in‑place action execution that mutates transaction objects.**

Securo automates financial transaction categorization without machine learning, instead relying on a **deterministic rule engine** that lives entirely on the back‑end. This approach gives users transparent, editable logic while maintaining consistent, reproducible results. The system is implemented across a tightly integrated pipeline of Python services and TypeScript utilities in the `securo-finance/securo` repository.

## Rule Definition: JSON‑Based Logical Structure

Rules in Securo are stored as structured JSON objects defined by the `Rule` model in [`backend/app/models/rule.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/rule.py). Each rule consists of three core components.

**Top‑level logical operator.** The `conditions_op` field specifies either `and` or `or` to join all top‑level conditions.

**Ordered condition nodes.** A nested list of **leaf conditions** or **grouped conditions** that evaluate against transaction fields. Groups enable mixed `AND`/`OR` logic within a single rule—critical for expressions like "type = debit AND (description contains 'UBER' OR '99POP')".

**Action list.** Operations that modify the matched transaction: `set_category`, `set_description`, `append_notes`, `set_payby`, or `ignore`.

The Pydantic schema in [`backend/app/schemas/rule.py`](https://github.com/securo-finance/securo/blob/main/backend/app/schemas/rule.py) validates all incoming API payloads before persistence.

## Condition Matching: Two‑Level Hierarchy with Text Normalization

The core evaluation logic resides in [`backend/app/services/rule_engine.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/rule_engine.py). When a transaction arrives, `evaluate_conditions` traverses every condition node using a **deliberately capped two‑level hierarchy** that prevents arbitrarily deep recursion while supporting complex boolean logic.

### Leaf Condition Evaluation

`_match_condition` handles individual comparisons through a strict normalization pipeline:

1. **`_normalize`** – Converts values to consistent representations
2. **`_strip_accents`** – Removes diacritics for accent‑insensitive matching
3. **Operator application** – `contains`, `starts_with`, `regex`, `gt`, `lt`, `equals`, `not_equals`

This normalization ensures "café" matches "cafe" and "UBER" matches "über" regardless of input source.

### Grouped Condition Evaluation

`_match_group` evaluates nested condition groups using their own operator against aggregated leaf results. This two‑level design—conditions within groups, groups within rules—balances expressiveness with predictable performance.

```python

# Back‑end: evaluating a transaction against a rule

tx = Transaction(...)
if evaluate_conditions(rule['conditions_op'], rule['conditions'], tx):
    category_set = apply_rule_actions(rule['actions'], tx, category_already_set=False)

```

## Action Execution: In‑Place Transaction Mutation

After condition evaluation succeeds, `apply_rule_actions` (same engine file) executes each action sequentially. The implementation handles five operation types:

| Action | Behavior | Field Modified |
|--------|----------|--------------|
| `set_category` | Writes UUID to `tx.category_id` if category not hidden | `category_id` |
| `set_description` | Updates description, marks as rule‑managed | `description`, `description_source` |
| `set_payby` | Sets payment method identifier | `payby` |
| `append_notes` | Adds text to transaction notes | `notes` |
| `ignore` | Flags transaction for exclusion | `ignored` |

The function returns a boolean flag indicating whether a category was already set. This enables **first‑match precedence**: subsequent rules can check `category_already_set` and skip redundant category assignments.

## Rule Service Orchestration: Priority‑Ordered Evaluation

The [`backend/app/services/rule_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/rule_service.py) module orchestrates the full pipeline:

1. Loads all active rules from the database
2. Orders by **priority value** (lower numbers execute first)
3. Iterates transactions through `evaluate_conditions` → `apply_rule_actions`
4. Commits mutated `Transaction` objects

This service design separates rule retrieval and ordering from pure evaluation logic, making both components independently testable.

## Front‑End Integration: Live Preview Utilities

Securo mirrors back‑end normalization in the browser to support **rule preview without server round‑trips**. The [`frontend/src/lib/rule-match-utils.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/rule-match-utils.ts) module exports `normalizeRuleMatchValue`:

```typescript
// Front‑end: building a rule that categorises Uber rides
const myRule = {
  name: 'Uber rides',
  priority: 10,
  conditions_op: 'and',
  conditions: [
    { field: 'type', op: 'equals', value: 'debit' },
    {
      op: 'or',
      conditions: [
        { field: 'description', op: 'contains', value: 'UBER' },
        { field: 'description', op: 'contains', value: 'UBER*' },
      ],
    },
  ],
  actions: [{ op: 'set_category', value: 'c1f2e3d4‑5a6b‑7c8d‑9e0f‑111213141516' }],
};

```

The rule builder UI in [`frontend/src/components/rule-dialog.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/components/rule-dialog.tsx) uses `previewableActions` from [`frontend/src/lib/rule-form-utils.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/rule-form-utils.ts) to filter incomplete actions before submission.

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`backend/app/models/rule.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/rule.py) | SQLAlchemy `Rule` model with JSON columns |
| [`backend/app/schemas/rule.py`](https://github.com/securo-finance/securo/blob/main/backend/app/schemas/rule.py) | Pydantic validation schemas |
| [`backend/app/services/rule_engine.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/rule_engine.py) | `evaluate_conditions`, `_match_condition`, `_match_group`, `apply_rule_actions` |
| [`backend/app/services/rule_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/rule_service.py) | Rule loading, priority ordering, transaction processing |
| [`backend/app/api/rules.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/rules.py) | REST endpoints for CRUD and testing |
| [`frontend/src/lib/rule-match-utils.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/rule-match-utils.ts) | Text normalization for preview matching |
| [`frontend/src/lib/rule-form-utils.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/rule-form-utils.ts) | Action filtering and form parsing |
| [`frontend/src/components/rule-dialog.tsx`](https://github.com/securo-finance/securo/blob/main/frontend/src/components/rule-dialog.tsx) | Rule composition interface |

## Summary

- **Rule engine location**: Pure‑Python back‑end in [`backend/app/services/rule_engine.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/rule_engine.py)—no ML dependencies
- **Condition flexibility**: Two‑level hierarchy (leaf conditions + groups) with `and`/`or` mixing
- **Text matching**: Accent‑insensitive via NFKD normalization and diacritic stripping
- **Execution model**: Priority‑ordered rules with first‑match category precedence
- **Front‑end parity**: Browser utilities mirror back‑end normalization for instant preview

## Frequently Asked Questions

### What operators does Securo's rule engine support for condition matching?

The engine supports `contains`, `starts_with`, `regex`, `gt`, `lt`, `equals`, and `not_equals`. These operate on normalized, accent‑stripped text for string fields and direct comparison for numeric and date fields.

### Can rules combine AND and OR logic in a single condition set?

Yes. The two‑level hierarchy allows top‑level `conditions_op` (and/or) plus nested groups with their own operators. This supports expressions like "debit AND (UBER OR 99POP)" without arbitrary recursion depth.

### How does Securo prevent duplicate category assignments from multiple rules?

The `apply_rule_actions` function returns a `category_already_set` flag. The rule service passes this through subsequent evaluations, so later rules can respect first‑match precedence or override it based on business logic.

### Does the rule engine require server round‑trips to preview matches?

No. The front‑end implements identical normalization logic in [`frontend/src/lib/rule-match-utils.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/rule-match-utils.ts), enabling live preview during rule construction. Only final rule persistence requires API calls to [`backend/app/api/rules.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/rules.py).