This guide mirrors
Command-line Accounting in Context
but describes how to achieve the same outcomes with beancount_ex in an Elixir
application.
What accounting gives you
Double-entry bookkeeping answers recurring questions:
| Question | Report / tool |
|---|---|
| Where did my money go this month? | Income statement |
| What am I worth right now? | Balance sheet (net worth) |
| What do I hold in each brokerage? | Holdings report |
| Does this account reconcile? | Journal + balance assertions |
| Can I file taxes from my data? | Income statement by tax year accounts |
beancount_ex does not replace your bank. It gives you a single integrated
ledger in Elixir that you can render, validate, and query.
The core activity: bookkeeping
Bookkeeping means recording every financial movement between accounts you define. Each transaction must balance to zero: every posting has a matching offset.
In Beancount text:
2026-05-23 * "CAFE" "Dinner"
Liabilities:Card -45.00 USD
Expenses:RestaurantIn Elixir:
Beancount.transaction(~D[2026-05-23], "*", "CAFE", "Dinner", [
Beancount.posting("Liabilities:Card", Decimal.new("-45.00"), "USD"),
Beancount.posting("Expenses:Restaurant", nil, nil)
])The elided Expenses:Restaurant amount is inferred at check time so the
transaction balances.
Account names use five top-level types: Assets, Liabilities, Income,
Expenses, and Equity. Colons separate hierarchy levels
(Assets:US:Bank:Checking).
Generating reports
Once transactions are in a ledger, reports aggregate positions:
{:ok, balances} = Beancount.balances(ledger)
{:ok, sheet} = Beancount.balance_sheet(ledger)
{:ok, income} = Beancount.income_statement(ledger)
{:ok, holdings} = Beancount.holdings(ledger)
{:ok, journal} = Beancount.journal(ledger, "Assets:Bank")Each returns a Beancount.Query.Result with columns and rows suitable for
tables in a UI. See Running reports.
With the optional Explorer dependency:
df = Beancount.Explorer.to_dataframe(balances)How the pieces fit in an app
A typical Elixir accounting UI pipeline:
User action / import
│
▼
Beancount.transaction/6, posting/4, open/4, …
│
▼
[directive structs] ──► Beancount.render/1 ──► .bean file / export
│
▼
Beancount.check/1 ──► validation errors for the UI
│
▼
Beancount.balances/1, income_statement/1, … ──► dashboards- Capture - build directives from forms, CSV importers, or LLM output.
- Validate -
check/1before persisting; surfaceresult.normalized.errors. - Persist -
Beancount.Storage.store/1(SQLite via Ecto), rendered text, or your own store. - Report - canned reports via
Beancount.balances/1and friends, ad-hoc filters viaBeancount.Queries, or arbitrary BQL viaBeancount.query/2.
Why a library instead of only .bean files?
- Type safety - structs and
Decimalcatch mistakes at compile time. - Composition - generate payroll, imports, or projections from Elixir code.
- Testing - property tests and golden files on rendered output.
- Engine swap - same API with
Engine.CLI(default) or nativeEngine.Elixirfor validation and canned reports without Python tooling.
Custom scripting
Upstream Beancount uses Python plugins. In beancount_ex you script in Elixir:
ledger
|> Enum.filter(&match?(%Beancount.Directives.Transaction{}, &1))
|> Enum.map(fn txn -> update_transaction(txn, &tag_restaurant/1) end)
|> Beancount.check()Parse existing files for batch transforms:
{:ok, directives} = Beancount.parse_file("legacy.bean")When to read upstream docs
The Beancount project docs cover syntax edge cases, tolerances, booking methods, and import tooling in depth. Use this library's Cookbook for Elixir equivalents of common patterns; follow upstream for accounting theory you are unsure about.
Next
- Cookbook - account naming, cash, salary, investments
- Running reports - wiring reports into a UI