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.