# `Beancount`
[🔗](https://github.com/thanos/beancount_ex/blob/v0.6.0/lib/beancount.ex#L1)

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 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.

# `directive`

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

A renderable Beancount directive struct.

# `query_return`

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

A successful BQL query result or a failure result.

# `account_value`

```elixir
@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`

```elixir
@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`

```elixir
@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 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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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"

# `commodity`

```elixir
@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"

# `custom`

```elixir
@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`

```elixir
@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"

# `event`

```elixir
@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"

# `holdings`

```elixir
@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`

```elixir
@spec include(String.t()) :: Beancount.Directives.Include.t()
```

Build an `include` directive.

## Examples

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

# `income_statement`

```elixir
@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`

```elixir
@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`

```elixir
@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"

# `open`

```elixir
@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: %{}
    }

# `option`

```elixir
@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`

```elixir
@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"

# `parse`

```elixir
@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!`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Build a `poptag` directive.

## Examples

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

# `posting`

```elixir
@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`

```elixir
@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"

# `push_tag`

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

Build a `pushtag` directive.

## Examples

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

# `query`

```elixir
@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 `c: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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Render a directive stream to deterministic `.bean` text.

Dispatches to the configured engine's `c: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`

```elixir
@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`

```elixir
@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
    "*"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
