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
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.
Balance sheet report. See Beancount.Report.balance_sheet/1.
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 custom directive.
Build a document directive.
Build an event directive.
Holdings report. See Beancount.Report.holdings/1.
Build an include directive.
Income statement report. See Beancount.Report.income_statement/1.
Per-account journal report. See Beancount.Report.journal/2.
Build a note directive.
Build an open directive.
Build an option directive.
Build a pad 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 price directive.
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).
Build a transaction directive.
Types
@type directive() :: Beancount.Directive.t()
A renderable Beancount directive struct.
@type query_return() :: {:ok, Beancount.Query.Result.t()} | {:error, Beancount.Result.t()}
A successful BQL query result or a failure result.
Functions
@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"
@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"
@spec balance(Date.t(), String.t(), Decimal.t(), String.t(), keyword()) :: Beancount.Directives.Balance.t()
Build a balance assertion directive.
Options:
:tolerance- explicit tolerance, rendered asAMOUNT ~ 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"
@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)
@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"))
@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)
@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)
@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)
@spec close(Date.t(), String.t(), keyword()) :: Beancount.Directives.Close.t()
Build a close directive.
Examples
iex> Beancount.close(~D[2026-12-31], "Assets:Bank").account
"Assets:Bank"
@spec commodity(Date.t(), String.t(), keyword()) :: Beancount.Directives.Commodity.t()
Build a commodity directive.
Examples
iex> Beancount.commodity(~D[2026-01-01], "USD").currency
"USD"
@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"
@spec document(Date.t(), String.t(), String.t(), keyword()) :: Beancount.Directives.Document.t()
Build a document directive.
Examples
iex> Beancount.document(~D[2026-01-01], "Assets:Bank", "stmt.pdf").path
"stmt.pdf"
@spec event(Date.t(), String.t(), String.t(), keyword()) :: Beancount.Directives.Event.t()
Build an event directive.
Examples
iex> Beancount.event(~D[2026-01-01], "location", "New York").type
"location"
@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"]
@spec include(String.t()) :: Beancount.Directives.Include.t()
Build an include directive.
Examples
iex> Beancount.include("accounts.bean").path
"accounts.bean"
@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)
@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")
@spec note(Date.t(), String.t(), String.t(), keyword()) :: Beancount.Directives.Note.t()
Build a note directive.
Examples
iex> Beancount.note(~D[2026-01-01], "Assets:Bank", "Called about fees").comment
"Called about fees"
@spec open(Date.t(), String.t(), [String.t()], keyword()) :: Beancount.Directives.Open.t()
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: %{}
}
@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"
@spec pad(Date.t(), String.t(), String.t(), keyword()) :: Beancount.Directives.Pad.t()
Build a pad directive.
Examples
iex> Beancount.pad(~D[2025-12-20], "Assets:Cash", "Equity:Opening").account
"Assets:Cash"
@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 .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"
@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"
@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"
@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"
@spec pop_tag(String.t()) :: Beancount.Directives.PopTag.t()
Build a poptag directive.
Examples
iex> Beancount.pop_tag("vacation").tag
"vacation"
@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- aBeancount.CostSpecstruct 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"
@spec price(Date.t(), String.t(), Decimal.t(), String.t(), keyword()) :: Beancount.Directives.Price.t()
Build a price directive.
Examples
iex> Beancount.price(~D[2026-01-01], "USD", Decimal.new("1.20"), "CAD").commodity
"USD"
@spec push_tag(String.t()) :: Beancount.Directives.PushTag.t()
Build a pushtag directive.
Examples
iex> Beancount.push_tag("vacation").tag
"vacation"
@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"]
@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"
@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"
)
@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 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"
@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"
@spec transaction( Date.t(), String.t(), String.t() | nil, String.t(), [Beancount.Directives.Posting.t()], keyword() ) :: Beancount.Directives.Transaction.t()
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
"*"