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

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. 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 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. 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.


# 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 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 module exports normalizeRuleMatchValue:

// 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 uses previewableActions from frontend/src/lib/rule-form-utils.ts to filter incomplete actions before submission.

Key Implementation Files

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

Summary

  • Rule engine location: Pure‑Python back‑end in 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, enabling live preview during rule construction. Only final rule persistence requires API calls to backend/app/api/rules.py.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →