An idiomatic Elixir interface to Beancount.
beancount_exis not a General Ledger. It is a compatibility layer and behavioral oracle: it constructs Beancount directives as typed Elixir structs, renders them to deterministic.beantext, and validates them through a configurable engine. The default engine wraps real Beancount (bean-check/bean-query); the nativeBeancount.Engine.Elixircan replace it without changing the public API. Optional Ecto storage (Beancount.Storage) persists directives to SQLite when you need it.
Why this library exists
A native Elixir General Ledger needs something trustworthy to be validated
against. Beancount is a mature, battle-tested double-entry accounting system.
By wrapping it behind a stable Elixir API, beancount_ex:
- gives applications an idiomatic way to construct and check ledgers today, and
- becomes the oracle that the native engine must agree with.
Public API: Beancount.*
|
+-------------+-------------+
v v
Directive DSL Engine Behaviour
| |
v v
Renderer Beancount.Engine
| +--------+--------+
| v v
| Engine.CLI Engine.Elixir
| (bean-check) (native booking)
|
v (opt-in)
Ecto Storage
(SQLite/Postgres)
Storage / QueriesInstallation
def deps do
[
{:beancount_ex, "~> 0.6"},
# optional: Explorer DataFrames for report tables (see guides/accounting/running_reports.md)
{:explorer, "~> 0.11"}
]
endTo run checks and queries you also need Beancount installed (only required at
runtime for Beancount.check/1, Beancount.query/2 and friends):
pip install beancount beanquery
bean-check comes from the beancount package; bean-query comes from the
separate beanquery package (required
for Beancount v3 query support).
Usage
ledger = [
Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"]),
Beancount.open(~D[2026-01-01], "Income:Salary", ["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")
])
]
# Deterministic rendering
bean = Beancount.render(ledger)
# 2026-01-01 open Assets:Bank USD
#
# 2026-01-01 open Income:Salary USD
#
# 2026-01-31 * "Employer" "Salary"
# Assets:Bank 5000 USD
# Income:Salary -5000 USD
# Validation through the configured engine
{:ok, result} = Beancount.check(ledger)
# Parse `.bean` text
{:ok, directives} = Beancount.parse_text(bean)
# Query (BQL) and reports
{:ok, result} = Beancount.query(ledger, "SELECT account, sum(position) GROUP BY account")
{:ok, balances} = Beancount.balances(ledger)
{:ok, income} = Beancount.income_statement(ledger)
# Optional: Explorer DataFrame (requires {:explorer, "~> 0.11"})
# df = Beancount.Explorer.to_dataframe(balances)
# Optional: persist directives to the built-in SQLite store
{:ok, 3} = Beancount.Storage.store(ledger)
loaded = Beancount.Storage.load()
Beancount.Queries.list_opens(prefix: "Assets")The public API is Beancount. There is intentionally no public
BeancountEx module and you never need to reference the internal
Beancount.Directives namespace.
Configuration
config :beancount_ex,
engine: Beancount.Engine.CLI,
bean_check_path: "bean-check",
bean_query_path: "bean-query"Testing
mix test passes without Beancount installed: unit, property and
golden-file rendering tests have no external dependency.
mix test # unit + property + golden (no Beancount needed)
mix test --include beancount # also runs integration tests (needs bean-check)
mix beancount.golden.update # regenerate golden fixtures
Guides
Accounting (build a UI or ledger)
For programmers building accounting features:
- Accounting guides index
- Getting started
- In context
- Cookbook
- Running reports
- Livebook: Getting started, Cookbook, Parsing, Reporting
Library (internals and testing)
- Library guides index
- Parsing, Rendering, Engines
- Querying, Queries, Reporting, Booking
- Storage, Golden files, Property testing
- Oracle strategy, Reconciliation, Performance
License
Released under the MIT License.