Skip to content

Coming from Beancount

Zhang reads beancount ledgers directly. You can point it at your main.bean and keep editing your files as before: Zhang shows them in its web UI, and what you record there is written back in beancount syntax.

This page shows how to serve a beancount ledger and lists, in Compatibility, where Zhang and beancount differ.

Give zhang serve the directory of your ledger and the name of its main file:

Terminal window
zhang serve /path/to/ledger --endpoint main.bean

With Docker, mount the directory at /data and add --endpoint after the image name:

Terminal window
docker run --name zhang -v "/path/to/ledger:/data" -p "8000:8000" kilerd/zhang:latest --endpoint main.bean
  • A main file ending in .bean, .bc or .beancount makes Zhang read the whole ledger as beancount: the main file and every file it includes, whatever their extension. A ledger is either beancount or Zhang syntax, never a mix.
  • Open http://localhost:8000. The Overview item in the sidebar shows how many problems Zhang found, and the Overview page lists them. Go through them first: most come from the differences below.
  • What you record in the web UI goes to a file with your main file’s extension, by default data/{{year}}/{{month_str}}.bean, written in beancount syntax. See Recording Transactions.

Installation lists every option of zhang serve.

Zhang reads the metadata of a transaction in a beancount file the way beancount and Fava do: metadata before the first posting belongs to the transaction, and every metadata line after a posting belongs to that posting, however it is indented.

2024-01-02 * "Cafe" "lunch"
invoice: "2024-001" ; the transaction's
Assets:Cash -10 CNY
receipt: "r-17" ; the Assets:Cash posting's
Expenses:Food 10 CNY
category: "meals" ; the Expenses:Food posting's

Older versions of Zhang wrote a transaction’s metadata after its postings. Such a file now reads differently in Zhang: that metadata belongs to the last posting, which is how Fava has always read it. Two keys keep working as before:

  • time: a time after the last posting, at the postings’ indentation as older Zhang wrote it, is still the transaction’s time of day, unless the transaction has a time of its own or another posting has one. A time indented deeper than its posting stays the posting’s.
  • document: a document on a posting is a document of its transaction, so documents attached to a transaction stay attached.

When Zhang writes a transaction, for example after you edit it in the web UI, it writes the transaction’s metadata before the postings and each posting’s metadata right under it, so the file reads the same in Zhang, beancount and Fava. See Transaction.

Beancount In Zhang
open Read, with its commodities and booking method (open Assets:Broker HOOL "FIFO"). The commodities must be declared, but postings in other commodities are not reported.
close Read. Closing an account that still holds something is reported as CloseNonZeroAccount.
commodity Read, and required: see Commodities must be declared.
transactions Read, with the flags *, ! and the other Beancount flags, on the transaction and on a posting, the txn keyword, tags, links, costs ({}, {{}}, with a date and a label) and prices (@, @@).
balance Read, with an optional ~ tolerance. Exact without one: see Balance assertions are exact.
pad Read, paired with the balance it serves: see Pads.
note, event Read.
document Read. The path is relative to the file that holds the directive, as in beancount: see Document paths.
price Read, for valuations in queries and the Commodities page.
query Read: the queries appear in the Saved menu of the Query page.
custom Read. custom "budget" … defines budgets: see Budgets.
option Read. Only some options have an effect: see Options.
plugin Python plugins do not run: see Plugins.
include Read, including * patterns such as include "2024/*.bean".
pushtag, poptag, pushmeta, popmeta Read.

A time: "HH:MM:SS" metadata entry gives a directive a time of day, except a balance or a pad, whose time Zhang ignores, as beancount does: see Balance times.

The metadata values beancount reads without quotes, an account, a currency, a number or an arithmetic expression, an amount, a date, a tag, TRUE, FALSE and NULL, are read as written and kept as text, on a transaction, on a posting and on every other directive. counterpart: Assets:Bank is the text Assets:Bank, limit: 10.00 USD the text 10.00 USD, and 1 + 2 stays 1 + 2, where beancount computes 3. A tag value keeps its # and TRUE, FALSE and NULL stay those words, where beanquery shows True, False and an empty value. Zhang writes every such value back as it was written.

Zhang cannot read the following. A file using them does not load at all:

  • a cost with only a date or a label, such as {2024-01-01} or {"lot-1"}. Write the cost first: {100.00 USD, 2024-01-01};
  • compound costs, {100 # 9.95 USD}, and {*}.

Zhang uses these options of a beancount ledger:

  • title, shown in the web UI;
  • operating_currency, but only one: Zhang keeps the last one and reports every other as MultipleOperatingCurrencyDetect;
  • account_previous_balances, account_previous_earnings, account_previous_conversions, account_current_earnings, account_current_conversions and conversion_currency, in the accounting periods of queries.

Every other beancount option, such as booking_method, inferred_tolerance_default or documents, is listed on the Settings page and otherwise ignored. Zhang has options of its own, such as default_booking_method and timezone, which beancount ignores in turn. See Options.

Every commodity a posting weighs in, a price names or an open lists must have a commodity directive. The operating currency is declared by its option. Beancount does not require commodity directives; Zhang reports a missing one as CommodityDoesNotDefine. Add one per commodity, dated before its first use:

1970-01-01 commodity HOOL

Beancount lets a balance pass when the difference is within a tolerance it derives from the number of decimals: balance Assets:A 10.00 USD passes on a balance of 10.004 USD. Zhang does not: an assertion holds only on the exact amount, or within the tolerance you write after ~, such as 10.00 ~ 0.005 USD. See Balances and Padding.

Transactions balance at the commodity’s precision

Section titled “Transactions balance at the commodity’s precision”

A transaction balances when, in each commodity, its weights add up to zero once rounded at the commodity’s precision: the precision metadata of its commodity directive, 2 decimals without one. Beancount derives this tolerance from the numbers written in the transaction instead. Give commodities with finer amounts, such as BTC, a precision of their own. See Lots and Cost Basis.

Zhang books with FIFO unless told otherwise, while beancount’s default is STRICT. For beancount’s behavior, add option "default_booking_method" "STRICT"; beancount’s own booking_method option is not read. NONE, AVERAGE and AVERAGE_ONLY are not implemented: an account using one gets an error and books with the default method. Lot labels select lots as in beancount: a sale written {, "first"} reduces the lot labelled first. See Lots and Cost Basis.

Zhang pairs each pad with the balance entries it serves as beancount does: the next balance of the account in each commodity on a later day, up to the account’s next pad, never one on the same day as the pad. The padding transaction is dated on the pad, the pad and its balance may be in different files, and a pad that serves no balance is reported as UnusedPad. The differences:

  • A pad brings the account to exactly the asserted amount, even within an explicit ~ tolerance, where beancount pads nothing.
  • Only an assertion on the padded account itself uses the pad: beancount also lets an assertion on a sub-account use up the pad of its parent account.
  • Of two pads of an account on the same day in different files, the one Zhang orders last pads, which may not be the one beancount uses.

See The pad directive, and Balance for the cases of parent accounts and sub-accounts.

Zhang checks a balance at the start of its date, before the transactions of that day, as beancount does, and ignores its time metadata. Earlier versions of Zhang checked it at that time, after the transactions of the day before it: where that changes what a balance checks, it is listed with a BalanceTimeIgnored notice. Date such a balance on the next day to check it after the day’s transactions.

Zhang reads the path of a document relative to the file that holds it, as beancount does, and the documents you upload are written that way. Earlier versions of Zhang wrote the paths of uploaded documents relative to the ledger root, into files like data/2026/10.bean, which beancount reports as missing. Zhang still opens those documents. On the local disk, it lists a DocumentPathRelativeToRoot notice on each, with the path to write instead, and reports a document found nowhere as DocumentNotFound. See Document.

Only price directives give prices. The prices written on postings with @ and @@ do not, as they would with beancount’s implicit_prices plugin.

  • Closing an account that still holds something is reported as an error. Beancount allows it.
  • Postings in a commodity that the account’s open does not list are not reported. Beancount reports them.
  • Account names must start with Assets, Liabilities, Equity, Income or Expenses. The name_assets… options, which rename them in beancount, are not read.
  • In a quoted string, a backslash that does not start an escape is kept: "\d" stays \d, where beancount drops the backslash.

Beancount’s plugins are Python code, and Zhang does not run them, including the ones that ship with beancount, such as auto_accounts or implicit_prices. Without them:

  • open every account explicitly, instead of relying on auto_accounts;
  • check what other plugins did for you, and do it in the ledger or with a query.

plugin lines stay harmless as long as the ledger does not enable Zhang’s own plugins: with option "features.plugin" "true", Zhang tries to load every plugin as a WASM module, and a Python module name makes the whole ledger fail to load. Zhang’s plugins are WASM modules: see Using Plugins.

Zhang reads budgets written as custom directives in the form beancount accepts: custom "budget" "Food" "CNY", custom "budget-add" "Food" 2000 CNY, custom "budget-transfer" "Fun" "Food" 300 CNY and custom "budget-close" "Food", with a quoted type and quoted names, and writes budgets that way, so bean-check and Fava accept the ledger. The unquoted form earlier versions of Zhang wrote, custom budget Food CNY, is still read, but beancount rejects it with Invalid token: switch to the quoted form. See Budget.

These work in Zhang and have no equivalent in beancount:

  • budgets, as above;
  • a time of day on entries, which a beancount ledger carries in time metadata;
  • Zhang’s WASM plugins and its query language extensions, such as the #budgets and #errors tables.

A Zhang ledger (main.zhang) has more syntax of its own, such as balance … with pad and dates with a time; beancount cannot read it. Zhang has no command to convert between the two syntaxes, so keep your ledger in beancount syntax if you want to keep using beancount tools.