beancount_ex lets you build Beancount ledgers as typed Elixir data, render deterministic .bean text, validate entries, and run reports. This guide is the entry point for the accounting track; see Library guides for parser and engine internals.

Install

def deps do
  [
    {:beancount_ex, "~> 0.6"},
    # optional: Explorer DataFrames for report tables in Livebook or Phoenix
    {:explorer, "~> 0.11"}
  ]
end

Validation and BQL reports require either:

  • the Beancount toolchain (pip install beancount beanquery), using the default Beancount.Engine.CLI, or
  • the native engine (config :beancount_ex, engine: Beancount.Engine.Elixir), which implements booking, balance assertions, and canned reports without shelling out.

Build a ledger

Constructors live on the Beancount module. Amounts are Decimal, dates are Date, accounts and commodities are strings.

ledger = [
  Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"]),
  Beancount.open(~D[2026-01-01], "Income:Salary", ["USD"]),
  Beancount.open(~D[2026-01-01], "Equity:Opening", ["USD"]),
  Beancount.transaction(~D[2026-01-31], "*", "Employer", "Salary", [
    Beancount.posting("Assets:Bank", Decimal.new("5000"), "USD"),
    Beancount.posting("Income:Salary", Decimal.new("-5000"), "USD")
  ])
]

Render

Beancount.render(ledger)

Rendering is deterministic: the same directive list always produces byte-identical output.

Check

case Beancount.check(ledger) do
  {:ok, result} -> result.status
  {:error, result} -> result.normalized.errors
end

You can also validate existing text or files:

Beancount.check_text("2026-01-01 open Assets:Bank USD\n")
Beancount.check_file("ledger.bean")

Parse and round-trip

Import .bean text from disk or user input:

{:ok, directives} = Beancount.parse_text(bean_text)
Beancount.render(directives) == bean_text  # after normalization

Next steps