Axiomatic

DSL Rules

Write declarative posting rules using the Axiomatic DSL to automate journal entry creation.

Overview

The Axiomatic rule 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 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
  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

A book can be assigned a specific rule pack. When assigned, only rules from that pack are evaluated. Otherwise, all published packs visible to the entity are considered.

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

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