Beancount (beancount_ex v0.6.0)

Copy Markdown View Source

Idiomatic Elixir interface to Beancount.

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 this public API.

Optional persistence: Beancount.Storage stores directives in SQLite via Ecto; Beancount.Queries reads them back without running the booking engine.

Quick start

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")
  ])
]

bean = Beancount.render(ledger)
{:ok, result} = Beancount.check(ledger)

The constructor functions (open/4, transaction/6, posting/4, ...) build the typed structs under Beancount.Directives. You never need to reference that namespace directly.

Summary

Types

A renderable Beancount directive struct.

A successful BQL query result or a failure result.

Functions

Wrap an account name for use in custom/4 values.

Wrap a commodity amount for use in custom/4 values.

Build a balance assertion directive.

Account balances report. See Beancount.Report.balances/1.

Render directives and validate the result through the configured engine.

Validate a .bean file on disk through the configured engine.

Validate raw .bean text through the configured engine.

Build a close directive.

Build a commodity directive.

Build a document directive.

Build an event directive.

Build an include directive.

Per-account journal report. See Beancount.Report.journal/2.

Build an option directive.

Parse a directive list or .bean text. See Beancount.Parser.parse/1.

Parse .bean text, raising on failure. See Beancount.Parser.parse!/1.

Parse a .bean file from disk. See Beancount.Parser.parse_file/1.

Parse .bean text into directives. See Beancount.Parser.parse_text/1.

Build a plugin directive.

Build a poptag directive.

Build a posting (a leg of a transaction).

Build a pushtag directive.

Run a BQL query against a directive stream.

Build a query directive storing a named BQL query in the ledger.

Run a BQL query against a .bean file on disk through the configured engine.

Run a BQL query against raw .bean text through the configured engine.

Render a directive stream to deterministic .bean text.

Wrap a tag name for use in custom/4 values (rendered as #tag).

Types

directive()

@type directive() :: Beancount.Directive.t()

A renderable Beancount directive struct.

query_return()

@type query_return() ::
  {:ok, Beancount.Query.Result.t()} | {:error, Beancount.Result.t()}

A successful BQL query result or a failure result.

Functions

account_value(name)

@spec account_value(String.t()) :: Beancount.Value.Account.t()

Wrap an account name for use in custom/4 values.

Examples

iex> Beancount.account_value("Assets:Bank").name
"Assets:Bank"

amount_value(number, currency)

@spec amount_value(Decimal.t(), String.t()) :: Beancount.Value.Amount.t()

Wrap a commodity amount for use in custom/4 values.

Examples

iex> v = Beancount.amount_value(Decimal.new("42"), "USD")
iex> v.currency
"USD"

balance(date, account, amount, currency, opts \\ [])

Build a balance assertion directive.

Options:

  • :tolerance - explicit tolerance, rendered as AMOUNT ~ TOLERANCE CURRENCY.
  • :metadata - a map of metadata key/values.

Examples

iex> Beancount.balance(~D[2026-01-31], "Assets:Bank", Decimal.new("5000"), "USD").currency
"USD"

balance_sheet(ledger)

@spec balance_sheet([directive()] | binary()) :: query_return()

Balance sheet report. See Beancount.Report.balance_sheet/1.

Examples

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

{:ok, %Beancount.Query.Result{}} =
  ledger |> Beancount.render() |> then(&Beancount.Report.balance_sheet/1)

balances(ledger)

@spec balances([directive()] | binary()) :: query_return()

Account balances report. See Beancount.Report.balances/1.

Examples

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], "*", nil, "Salary", [
    Beancount.posting("Assets:Bank", Decimal.new("100"), "USD"),
    Beancount.posting("Income:Salary", Decimal.new("-100"), "USD")
  ])
]

{:ok, %Beancount.Query.Result{}} =
  ledger |> Beancount.render() |> then(&Beancount.Engine.Elixir.query(&1, "SELECT account, sum(position) GROUP BY account"))

check(directives)

@spec check([directive()]) ::
  {:ok, Beancount.Result.t()} | {:error, Beancount.Result.t()}

Render directives and validate the result through the configured engine.

Returns {:ok, result} for a valid ledger and {:error, result} otherwise.

Examples

With the configured engine:

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], "*", nil, "Salary", [
    Beancount.posting("Assets:Bank", Decimal.new("100"), "USD"),
    Beancount.posting("Income:Salary", Decimal.new("-100"), "USD")
  ])
]

{:ok, %Beancount.Result{status: :ok}} = Beancount.check(ledger)

check_file(path)

@spec check_file(Path.t()) ::
  {:ok, Beancount.Result.t()} | {:error, Beancount.Result.t()}

Validate a .bean file on disk through the configured engine.

Examples

path = Path.join(System.tmp_dir!(), "check_example.bean")

File.write!(path, """
2026-01-01 open Assets:Bank USD
2026-01-01 open Income:Salary USD

2026-01-31 * "Employer" "Salary"
  Assets:Bank     100 USD
  Income:Salary  -100 USD
""")

{:ok, %Beancount.Result{status: :ok}} = Beancount.Engine.Elixir.check_file(path)

check_text(text)

@spec check_text(binary()) ::
  {:ok, Beancount.Result.t()} | {:error, Beancount.Result.t()}

Validate raw .bean text through the configured engine.

Examples

text = """
2026-01-01 open Assets:Bank USD
2026-01-01 open Income:Salary USD

2026-01-31 * "Employer" "Salary"
  Assets:Bank     100 USD
  Income:Salary  -100 USD
"""

{:ok, %Beancount.Result{status: :ok}} = Beancount.Engine.Elixir.check(text)

close(date, account, opts \\ [])

Build a close directive.

Examples

iex> Beancount.close(~D[2026-12-31], "Assets:Bank").account
"Assets:Bank"

commodity(date, currency, opts \\ [])

Build a commodity directive.

Examples

iex> Beancount.commodity(~D[2026-01-01], "USD").currency
"USD"

custom(date, type, values \\ [], opts \\ [])

@spec custom(Date.t(), String.t(), [term()], keyword()) ::
  Beancount.Directives.Custom.t()

Build a custom directive.

Examples

iex> Beancount.custom(~D[2026-01-01], "budget", ["monthly"]).type
"budget"

document(date, account, path, opts \\ [])

Build a document directive.

Examples

iex> Beancount.document(~D[2026-01-01], "Assets:Bank", "stmt.pdf").path
"stmt.pdf"

event(date, type, description, opts \\ [])

Build an event directive.

Examples

iex> Beancount.event(~D[2026-01-01], "location", "New York").type
"location"

holdings(ledger)

@spec holdings([directive()] | binary()) :: query_return()

Holdings report. See Beancount.Report.holdings/1.

Examples

ledger = [
  Beancount.open(~D[2026-01-01], "Assets:Stocks", ["AAPL"], booking: "FIFO"),
  Beancount.open(~D[2026-01-01], "Assets:Cash", ["USD"]),
  Beancount.transaction(~D[2026-01-02], "*", nil, "Buy", [
    Beancount.posting("Assets:Stocks", Decimal.new("10"), "AAPL",
      cost: %{amount: Decimal.new("150"), currency: "USD"}
    ),
    Beancount.posting("Assets:Cash", Decimal.new("-1500"), "USD")
  ])
]

{:ok, %Beancount.Query.Result{columns: cols}} = Beancount.Report.holdings(ledger)
cols
# => ["account", "units", "cost"]

include(path)

Build an include directive.

Examples

iex> Beancount.include("accounts.bean").path
"accounts.bean"

income_statement(ledger)

@spec income_statement([directive()] | binary()) :: query_return()

Income statement report. See Beancount.Report.income_statement/1.

Examples

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

{:ok, %Beancount.Query.Result{}} = Beancount.Report.income_statement(ledger)

journal(ledger, account)

@spec journal([directive()] | binary(), String.t()) :: query_return()

Per-account journal report. See Beancount.Report.journal/2.

Examples

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], "*", nil, "Salary", [
    Beancount.posting("Assets:Bank", Decimal.new("100"), "USD"),
    Beancount.posting("Income:Salary", Decimal.new("-100"), "USD")
  ])
]

{:ok, %Beancount.Query.Result{}} = Beancount.Report.journal(ledger, "Assets:Bank")

note(date, account, comment, opts \\ [])

Build a note directive.

Examples

iex> Beancount.note(~D[2026-01-01], "Assets:Bank", "Called about fees").comment
"Called about fees"

open(date, account, currencies \\ [], opts \\ [])

Build an open directive.

Options

  • :booking - booking method, e.g. "STRICT".
  • :metadata - a map of metadata key/values.

Examples

iex> Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"])
%Beancount.Directives.Open{
  date: ~D[2026-01-01],
  account: "Assets:Bank",
  currencies: ["USD"],
  booking: nil,
  metadata: %{}
}

option(name, value)

@spec option(String.t(), term()) :: Beancount.Directives.Option.t()

Build an option directive.

Common keys: title, operating_currency, inferred_tolerance_default, inferred_tolerance_multiplier, infer_tolerance_from_cost, tolerance_multiplier.

Examples

iex> Beancount.option("title", "My Ledger").name
"title"

pad(date, account, source_account, opts \\ [])

Build a pad directive.

Examples

iex> Beancount.pad(~D[2025-12-20], "Assets:Cash", "Equity:Opening").account
"Assets:Cash"

parse(input)

@spec parse([directive()] | binary()) ::
  {:ok, [directive()]} | {:error, Beancount.Parser.Error.t()}

Parse a directive list or .bean text. See Beancount.Parser.parse/1.

Examples

iex> {:ok, directives} = Beancount.parse([Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"])])
iex> hd(directives).account
"Assets:Bank"

iex> {:ok, directives} = Beancount.parse_text("2026-01-01 open Assets:Bank USD\n")
iex> hd(directives).account
"Assets:Bank"

parse!(text)

@spec parse!(binary()) :: [directive()]

Parse .bean text, raising on failure. See Beancount.Parser.parse!/1.

Examples

iex> Beancount.parse!("2026-01-01 open Assets:Bank USD\n")
...> |> hd()
...> |> Map.get(:account)
"Assets:Bank"

parse_file(path)

@spec parse_file(Path.t()) ::
  {:ok, [directive()]} | {:error, Beancount.Parser.Error.t() | term()}

Parse a .bean file from disk. See Beancount.Parser.parse_file/1.

Examples

path = Path.join(System.tmp_dir!(), "parse_example.bean")
File.write!(path, "2026-01-01 open Assets:Bank USD\n")

{:ok, [%Beancount.Directives.Open{} = open]} = Beancount.parse_file(path)
open.account
# => "Assets:Bank"

parse_text(text)

@spec parse_text(binary()) ::
  {:ok, [directive()]} | {:error, Beancount.Parser.Error.t()}

Parse .bean text into directives. See Beancount.Parser.parse_text/1.

Examples

iex> {:ok, directives} = Beancount.parse_text("2026-01-01 commodity USD\n")
iex> hd(directives).currency
"USD"

plugin(module, config \\ nil)

@spec plugin(String.t(), String.t() | nil) :: Beancount.Directives.Plugin.t()

Build a plugin directive.

Examples

iex> Beancount.plugin("beancount.plugins.auto_accounts").module
"beancount.plugins.auto_accounts"

pop_tag(tag)

@spec pop_tag(String.t()) :: Beancount.Directives.PopTag.t()

Build a poptag directive.

Examples

iex> Beancount.pop_tag("vacation").tag
"vacation"

posting(account, amount \\ nil, currency \\ nil, opts \\ [])

@spec posting(String.t(), Decimal.t() | nil, String.t() | nil, keyword()) ::
  Beancount.Directives.Posting.t()

Build a posting (a leg of a transaction).

amount and currency may be nil for an elided amount. Options:

  • :cost - a Beancount.CostSpec struct or legacy %{amount:, currency:} map.
  • :price - a %{amount: Decimal.t(), currency: String.t(), type: :unit | :total} price.

  • :flag - a per-posting flag.
  • :metadata - a map of metadata key/values.

Examples

iex> Beancount.posting("Assets:Bank", Decimal.new("5000"), "USD").account
"Assets:Bank"

price(date, commodity, amount, currency, opts \\ [])

Build a price directive.

Examples

iex> Beancount.price(~D[2026-01-01], "USD", Decimal.new("1.20"), "CAD").commodity
"USD"

push_tag(tag)

@spec push_tag(String.t()) :: Beancount.Directives.PushTag.t()

Build a pushtag directive.

Examples

iex> Beancount.push_tag("vacation").tag
"vacation"

query(directives, bql)

@spec query([directive()], binary()) :: query_return()

Run a BQL query against a directive stream.

The directives are rendered to .bean text and the query is dispatched through the configured engine's Beancount.Engine.query/2.

Examples

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], "*", nil, "Salary", [
    Beancount.posting("Assets:Bank", Decimal.new("100"), "USD"),
    Beancount.posting("Income:Salary", Decimal.new("-100"), "USD")
  ])
]

bql = "SELECT account, sum(position) AS balance GROUP BY account ORDER BY account"

{:ok, %Beancount.Query.Result{columns: columns}} =
  ledger |> Beancount.render() |> then(&Beancount.Engine.Elixir.query(&1, bql))

columns
# => ["account", "balance"]

query_directive(date, name, bql, opts \\ [])

@spec query_directive(Date.t(), String.t(), String.t(), keyword()) ::
  Beancount.Directives.Query.t()

Build a query directive storing a named BQL query in the ledger.

Examples

iex> q = Beancount.query_directive(~D[2026-01-01], "balances", "SELECT account")
iex> q.name
"balances"

query_file(path, bql)

@spec query_file(Path.t(), binary()) :: query_return()

Run a BQL query against a .bean file on disk through the configured engine.

Examples

path = Path.join(System.tmp_dir!(), "query_example.bean")

File.write!(path, """
2026-01-01 open Assets:Bank USD
2026-01-01 open Income:Salary USD
2026-01-01 open Equity:Opening USD

2026-01-31 * "Employer" "Salary"
  Assets:Bank     100 USD
  Income:Salary  -100 USD
""")

{:ok, _} =
  Beancount.Engine.Elixir.query(
    File.read!(path),
    "SELECT account, sum(position) AS balance GROUP BY account ORDER BY account"
  )

query_text(text, bql)

@spec query_text(binary(), binary()) :: query_return()

Run a BQL query against raw .bean text through the configured engine.

Examples

text = """
2026-01-01 open Assets:Bank USD
2026-01-01 open Income:Salary USD
2026-01-01 open Equity:Opening USD

2026-01-31 * "Employer" "Salary"
  Assets:Bank     100 USD
  Income:Salary  -100 USD
"""

{:ok, result} =
  Beancount.Engine.Elixir.query(
    text,
    "SELECT account, sum(position) AS balance GROUP BY account ORDER BY account"
  )

result.columns
# => ["account", "balance"]

render(directives)

@spec render([directive()]) :: binary()

Render a directive stream to deterministic .bean text.

Dispatches to the configured engine's Beancount.Engine.render/1.

Examples

iex> Beancount.render([Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"])])
"2026-01-01 open Assets:Bank USD\n"

tag_value(name)

@spec tag_value(String.t()) :: Beancount.Value.Tag.t()

Wrap a tag name for use in custom/4 values (rendered as #tag).

Examples

iex> Beancount.tag_value("trip").name
"trip"

transaction(date, flag, payee, narration, postings, opts \\ [])

Build a transaction directive.

payee may be nil to render only a narration. Options:

  • :tags - list of tag strings (rendered as #tag).
  • :links - list of link strings (rendered as ^link).
  • :metadata - a map of metadata key/values.

Examples

iex> txn = Beancount.transaction(~D[2026-01-31], "*", "Employer", "Salary", [])
iex> txn.flag
"*"