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"}
]
endValidation and BQL reports require either:
- the Beancount toolchain (
pip install beancount beanquery), using the defaultBeancount.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
endYou 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 normalizationNext steps
- In context - motivation and how pieces fit together
- Cookbook - real-world transaction patterns
- Running reports - balances and statements for your UI