# beancount_ex
[](https://hex.pm/packages/beancount_ex)
[](https://hexdocs.pm/beancount_ex)
[](https://hex.pm/packages/beancount_ex)
[](https://github.com/thanos/beancount_ex/actions/workflows/ci.yml)
[](https://coveralls.io/github/thanos/beancount_ex)
An idiomatic Elixir interface to [Beancount](https://beancount.github.io/).
> **`beancount_ex` is not a General Ledger.** It is a compatibility layer and
> *behavioral oracle*: it constructs Beancount directives as typed Elixir
> structs, renders them to deterministic `.bean` text, and validates them
> through a configurable engine. The default engine wraps real Beancount
> (`bean-check` / `bean-query`); the native `Beancount.Engine.Elixir` can
> 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 / Queries
```
## Installation
```elixir
def deps do
[
{:beancount_ex, "~> 0.6"},
# optional: Explorer DataFrames for report tables (see guides/accounting/running_reports.md)
{:explorer, "~> 0.11"}
]
end
```
To run checks and queries you also need Beancount installed (only required at
runtime for `Beancount.check/1`, `Beancount.query/2` and friends):
```bash
pip install beancount beanquery
```
`bean-check` comes from the `beancount` package; `bean-query` comes from the
separate [`beanquery`](https://github.com/beancount/beanquery) package (required
for Beancount v3 query support).
## Usage
```elixir
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
```elixir
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.
```bash
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](guides/accounting/index.md)
- [Getting started](guides/accounting/getting_started.md)
- [In context](guides/accounting/in_context.md)
- [Cookbook](guides/accounting/cookbook.md)
- [Running reports](guides/accounting/running_reports.md)
- Livebook: [Getting started](guides/livebook/getting_started.livemd), [Cookbook](guides/livebook/accounting.livemd), [Parsing](guides/livebook/parsing.livemd), [Reporting](guides/livebook/reporting.livemd)
### Library (internals and testing)
- [Library guides index](guides/library.md)
- [Parsing](guides/parsing.md), [Rendering](guides/rendering.md), [Engines](guides/engines.md)
- [Querying](guides/querying.md), [Queries](guides/queries.md), [Reporting](guides/reporting.md), [Booking](guides/booking.md)
- [Storage](guides/storage.md), [Golden files](guides/golden_files.md), [Property testing](guides/property_testing.md)
- [Oracle strategy](guides/oracle_strategy.md), [Reconciliation](guides/reconciliation.md), [Performance](guides/performance.md)
## License
Released under the [MIT License](LICENSE).