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
- An event enters the system (e.g.
invoice_issued) - The posting engine searches for a matching rule by event type
- Rules are evaluated in priority order — the first match wins
- The matched rule's
wherecondition is checked against the event payload - If the condition passes, the
postblock generates debit/credit lines - Account roles are resolved to actual account IDs via role mappings
- 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
| Clause | Required | Description |
|---|---|---|
on "event_type" | Yes | The event type this rule matches |
priority N | No | Higher values are tried first (default: 0) |
where <expr> | No | Boolean condition that must be true for the rule to match |
let name = <expr> | No | Variable bindings for use in subsequent expressions |
post { ... } | Yes | Posting 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.amountCondition 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
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, % |
| Comparison | ==, !=, <, >, <=, >= |
| Boolean | and, or, not |
| String | ++ (concatenation) |
Built-in Functions
| Function | Description |
|---|---|
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_issuedevents 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 0If 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 Type | Typical Posting |
|---|---|
invoice_issued | DR Accounts Receivable, CR Revenue |
payment_received | DR Cash, CR Accounts Receivable |
payroll_processed | DR Expense, CR Cash |
vendor_bill | DR Expense, CR Accounts Payable |
vendor_payment | DR Expense, CR Cash |
debt_payment | DR Liability, CR Cash |
income_received | DR Cash, CR Income |
crypto_received | DR Crypto Cash, CR Revenue |
capital_contribution | DR Cash, CR Equity |
investment_made | DR 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.