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:
_normalize– Converts values to consistent representations_strip_accents– Removes diacritics for accent‑insensitive matching- 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:
- Loads all active rules from the database
- Orders by priority value (lower numbers execute first)
- Iterates transactions through
evaluate_conditions→apply_rule_actions - Commits mutated
Transactionobjects
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/ormixing - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →