Axiomatic

Rules

Categorize transactions and automate journal entry creation with Axiomatic's two-stage rule pipeline.

Overview

Axiomatic uses a two-stage rule pipeline to process imported transactions into journal entries:

Transaction → [Categorization] → Event → [Posting Rules] → Journal Entry
StagePurposeWho uses it
CategorizationDetermines what type of transaction this is (e.g. "JPMorgan Chase" → loan_payment)Everyone
PostingDetermines how to record the event as debits and creditsPower users / accountants

These two stages work together but are managed independently. The Rules page in Axiomatic has two tabs — one for each stage.

Stage 1: Categorization

Categorization rules automatically classify imported transactions based on counterparty name patterns. When a bank transaction is imported, the system checks if its counterparty matches any saved pattern before falling back to the default classification heuristics.

How It Works

  1. A transaction is imported (via Plaid, CSV, or API)
  2. The system checks the counterparty name against saved categorization patterns
  3. If a pattern matches, the transaction is classified with the pattern's event type
  4. If no pattern matches, the system uses built-in heuristics (account type, Plaid category, etc.)
  5. If the pattern specifies a target account, that account overrides the default role-based mapping

Creating Patterns

Patterns can be created in two ways:

  • From reclassification — after you reclassify a transaction in the account ledger drill-down, the system prompts you to save the counterparty as a categorization rule. You can optionally specify a target account. If other past transactions match the same counterparty, you can batch-reclassify them in one action.
  • Manually — from the Rules > Categorization tab, click "Add Rule" and specify the counterparty pattern, event type, and optional target account.

Pattern Matching

Patterns use case-insensitive substring matching. A pattern of JPMORGAN CHASE matches any counterparty containing that string (e.g. "JPMORGAN CHASE DES:Ext Trnsfr", "JPMorgan Chase Bank").

Patterns are evaluated in order of usage frequency — the most frequently matched pattern is checked first.

Account Override

Each pattern can optionally specify a target account. When set, the posting engine uses this specific account instead of the default role-based account mapping. This is useful for cases like:

  • "JPMORGAN CHASE" → loan_paymentAuto Loan account (instead of generic "Liability")
  • "ANYTIME FITNESS" → vendor_paymentHealth & Fitness expense account

API Reference

GET  /api/match-patterns?entityId=...
POST /api/match-patterns    { entityId, counterpartyPattern, mappedEventType, mappedAccountId? }
PATCH /api/match-patterns   { id, counterpartyPattern?, mappedEventType?, mappedAccountId? }
DELETE /api/match-patterns?id=...

Retroactive Reclassification

After creating a pattern, you can batch-apply it to past transactions:

GET /api/match-patterns/retroactive?entityId=...&counterparty=...&newEventType=...

This returns all existing events whose imported counterparty matches the pattern but have a different event type. Use the batch reclassify endpoint to apply:

POST /api/events/batch-reclassify
{ entityId, eventIds: [...], newEventType, accountOverride?, reason? }

Stage 2: Posting Rules (DSL)

The posting engine uses a purpose-built DSL (domain-specific language) to define how events become journal entries. Instead of manually creating journal entries for every transaction, you write rules that automatically match events and produce the correct debits and credits.

Rules are organized into rule packs — versioned containers that group related rules. Each rule matches an event type, evaluates optional conditions, and produces posting lines.

How Posting Rules Work

  1. An event enters the system (e.g. invoice_issued)
  2. The posting engine searches for a matching rule by event type
  3. Rules are evaluated in priority order — the first match wins
  4. The matched rule's where condition is checked against the event payload
  5. If the condition passes, the post block generates debit/credit lines
  6. Account roles are resolved to actual account IDs via role mappings (or the categorization pattern's account override)
  7. A journal entry is created

Rule Packs

Rule packs group related rules and control when they're active.

Statuses

  • Draft — editable, not used by the posting engine
  • Published — active and evaluated during posting
  • Superseded — replaced by a newer version, kept for audit
  • Deprecated — no longer in use

Layers

Packs are classified by scope:

  • Kernel — foundational rules shipped with the system
  • Standard — general-purpose rules applicable across entities
  • Industry — sector-specific rule sets (e.g. fund accounting)
  • Entity — rules created for a specific entity

Activating Packs

Rule packs must be activated for a book before their rules are evaluated during posting. Activation creates a link between a book and a pack via the book_rule_packs table.

  • Single-book entities — clicking Activate applies the pack to the entity's only book automatically.
  • Multi-book entities — the UI presents a book picker so you can activate a pack for specific books (e.g. activate a tax-basis pack only for the TAX book).

You can activate multiple packs for the same book. Rules are resolved across all activated packs by priority — the first matching rule wins.

To deactivate a pack, click Deactivate on an active pack. For multi-book entities, you can deactivate from individual books.

Custom entity packs (layer = ENTITY) are always shown in the Active section regardless of explicit activation.

PUT /api/rule-packs
{ "action": "activate_pack", "entityId": "...", "packId": "...", "bookId": "..." }
PUT /api/rule-packs
{ "action": "deactivate_pack", "entityId": "...", "packId": "...", "bookId": "..." }

Omit bookId to activate/deactivate for all books in the entity.

DSL Syntax

Basic Structure

rule "Rule Name" {
  on "event_type"
  priority 10
  where <condition>

  let variable = <expression>

  post {
    debit {
      account: <expression>
      amount: <expression>
      currency: <expression>
      memo: <expression>
    }
    credit {
      account: <expression>
      amount: <expression>
      currency: <expression>
      memo: <expression>
    }
  }
}

Rule Clauses

ClauseRequiredDescription
on "event_type"YesThe event type this rule matches
priority NNoHigher values are tried first (default: 0)
where <expr>NoBoolean condition that must be true for the rule to match
let name = <expr>NoVariable bindings for use in subsequent expressions
post { ... }YesPosting block with debit/credit lines

Accessing Event Data

Event payload fields are accessed via dot notation:

payload.total_amount
payload.currency
payload.counterparty
payload.line_items.amount

Condition Expressions

The where clause accepts boolean expressions:

where payload.currency == "USD"
where contains(payload.description, "advisory") and payload.total_amount > 1000
where payload.method == "crypto" or payload.currency == "USDC"

Operators

CategoryOperators
Arithmetic+, -, *, /, %
Comparison==, !=, <, >, <=, >=
Booleanand, or, not
String++ (concatenation)

Built-in Functions

FunctionDescription
coalesce(a, b)Returns the first non-null value
contains(str, substr)Case-insensitive substring check
starts_with(str, prefix)Prefix check
upper(str) / lower(str)Case conversion
abs(n) / min(a, b) / max(a, b)Numeric operations
round(n, scale, mode)Decimal rounding (HALF_UP, HALF_EVEN, FLOOR, CEILING, TRUNCATE)
account(role)Resolves a logical role to an account
is_null(val)Null check
between(val, lo, hi)Range check
concat(a, b, ...)Multi-argument string concatenation
to_string(val)Convert any value to string

Account Resolution

Use the account() function to reference accounts by their logical role:

account: account("accounts.receivable")
account: account("cash.operating")
account: account("accounts.revenue.advisory")

The role is resolved to a concrete account ID at posting time via your entity's role mappings.

Full Example

rule "Revenue Recognition — Advisory Invoice" {
  on "invoice_issued"
  priority 10
  where contains(payload.description, "advisory")

  let amt = payload.total_amount
  let ccy = coalesce(payload.currency, "USD")

  post {
    debit {
      account: account("accounts.receivable")
      amount: amt
      currency: ccy
      memo: "Invoice " ++ coalesce(payload.invoice_number, "") ++ " — " ++ coalesce(payload.counterparty, "")
    }
    credit {
      account: account("accounts.revenue.advisory")
      amount: amt
      currency: ccy
      memo: coalesce(payload.description, "")
    }
  }
}

This rule:

  • Matches invoice_issued events where the description contains "advisory"
  • Debits Accounts Receivable and credits Advisory Revenue
  • Uses the invoice amount and currency from the event payload
  • Builds a memo from the invoice number and counterparty name

Conditional Lines

Lines with a zero or empty amount are automatically excluded. Use if/then/else for more complex logic:

amount: if payload.tax_amount > 0 then payload.tax_amount else 0

If fewer than 2 lines survive filtering, no journal entry is created.

Priority and Matching

Rules are evaluated in descending priority order. The first rule whose conditions match wins. Use priority to layer specific rules above general-purpose ones:

rule "Crypto Invoice" {
  on "invoice_issued"
  priority 20
  where payload.currency == "USDC"
  ...
}

rule "Standard Invoice" {
  on "invoice_issued"
  priority 10
  ...
}

The crypto-specific rule at priority 20 is tried first. If the currency isn't USDC, it falls through to the standard rule at priority 10.

Common Event Types

These event types are typically covered by the default rule packs:

Event TypeTypical Posting
invoice_issuedDR Accounts Receivable, CR Revenue
payment_receivedDR Cash, CR Accounts Receivable
payroll_processedDR Expense, CR Cash
vendor_billDR Expense, CR Accounts Payable
vendor_paymentDR Expense, CR Cash
debt_paymentDR Liability, CR Cash
income_receivedDR Cash, CR Income
crypto_receivedDR Crypto Cash, CR Revenue
capital_contributionDR Cash, CR Equity
investment_madeDR Investment Asset, CR Cash

In-App Rule Editor

ENTITY-layer rules can be edited directly in the Axiomatic UI using the built-in DSL editor. The editor is powered by CodeMirror 6 and provides:

  • Syntax highlighting — keywords (rule, on, post, where, let, debit, credit), built-in functions, operators, strings, and numbers are color-coded.
  • Inline validation — as you type, the DSL source is validated against the Rust WASM engine. Errors are displayed below the editor in real time.
  • Test / Preview panel — paste a sample event payload (JSON) and dry-run the rule to see the posting lines it would produce, without creating actual journal entries. This uses the /api/dsl/evaluate endpoint.
  • Read-only mode — KERNEL and STANDARD pack rules are displayed in the editor but cannot be modified, preserving system integrity.

To edit a rule, navigate to Rules, expand a pack, and click on a rule. For ENTITY-layer packs, the editor is fully interactive. Changes are saved via the update_rule API action:

PUT /api/rule-packs
{ "action": "update_rule", "entityId": "...", "ruleId": "...", "dslSource": "..." }

The DSL source is re-parsed on save — the rule's name, eventType, and priority are automatically extracted from the updated source.

Creating a Rule Pack

POST /api/rule-packs
{
  "name": "My Custom Rules",
  "layer": "ENTITY",
  "entityId": "your-entity-id",
  "status": "DRAFT",
  "effectiveDate": "2025-01-01",
  "rules": [
    {
      "name": "Custom Revenue Rule",
      "eventType": "invoice_issued",
      "priority": 15,
      "dslSource": "rule \"Custom Revenue\" {\n  on \"invoice_issued\"\n  priority 15\n  post {\n    debit { account: account(\"accounts.receivable\") amount: payload.total_amount currency: coalesce(payload.currency, \"USD\") memo: \"Invoice\" }\n    credit { account: account(\"accounts.revenue.saas\") amount: payload.total_amount currency: coalesce(payload.currency, \"USD\") memo: \"Revenue\" }\n  }\n}"
    }
  ]
}

Set the status to PUBLISHED when the pack is ready for use.

On this page