Querying your books with BQL

ShippedWorks today.

Anything the reports do not answer, the Beancount Query Language will — a SQL-shaped language that runs against your ledger and returns rows, with no database to connect to.

Two ways to ask

balances is the shortcut. Give it nothing and you get every account with a balance; give it a filter and it narrows:

balances("Expenses:Food")

Under it is one BQL statement — SELECT account, sum(position) AS balance WHERE account ~ '<filter>' GROUP BY account ORDER BY account. The filter is a regular expression, not a prefix, so Expenses:Food and Food both match Expenses:Food:Groceries, and ^Assets does what you would expect.

run_query is the whole language. It takes BQL and returns rows.

The shape of a query

SELECT account, sum(position)
WHERE account ~ 'Expenses' AND year = 2026
GROUP BY account
ORDER BY account

It reads like SQL and is not SQL. There are no tables and no joins: the thing being selected from is always the stream of postings in your ledger, and the columns are posting attributes — date, account, narration, payee, position, year, month, flag, tags.

Queries worth keeping

Last twenty things that touched the current account:

SELECT date, payee, narration, position
WHERE account ~ 'Assets:Bank:Checking'
ORDER BY date DESC LIMIT 20

One category, month by month:

SELECT year, month, sum(position)
WHERE account ~ 'Expenses:Software'
GROUP BY year, month
ORDER BY year, month

Everything still flagged for review, because the agent was not confident enough to categorise it:

SELECT date, payee, narration, account, position
WHERE flag = '!'
ORDER BY date

That last one is the one to run after an import.

Where it runs

On your book’s machine, through POST /query, and a read scope token is enough — a read-only api-key can query and cannot write. A query that is not valid BQL comes back as a rejection carrying the parser’s message; nothing about a query changes the ledger.

What there is not

No saved queries. There is no place to name one and find it again. Keep them in a note, or ask the agent for the same thing again — it composes the query each time.

No scheduled or emailed results. You ask, or you open the browser.

No query results as a download. BQL rows come back as rows. The file formats are generate_report’s HTML page and Excel workbook, which are fixed reports rather than arbitrary queries.

No cross-book queries. BQL runs inside one book, on one machine. Two books are two questions.

No SQL access to the underlying data. There is no database to point a BI tool at — the ledger is a text file and BQL is the query interface to it.

Something here wrong or missing? Put it on the board — it is public, and the reply is in the thread.