Scriptable Workflows with Beancount and Fava
Beancount (a plain-text double-entry accounting tool) and Fava (its web interface) are highly extensible and scriptable. Their design allows you to automate financial tasks, generate custom reports, and set up alerts by writing Python scripts. In the words of one user, “I really like having my data in such a convenient format, and I like that I can automate things to my heart’s content. There is no API like a file on your disk; it’s easy to integrate with.” This guide will walk through creating scriptable workflows—from beginner-friendly automation to advanced Fava plugins.
Getting Started: Running Beancount as a Python Script
Before diving into specific tasks, ensure you have Beancount installed (e.g. via pip install beancount
). Since Beancount is written in Python, you can use it as a library in your own scripts. The general approach is:
-
Load your Beancount ledger: Use Beancount’s loader to parse the
.beancount
file into Python objects. For example:from beancount import loader
entries, errors, options_map = loader.load_file("myledger.beancount")
if errors:
print("Errors:", errors)This gives you a list of
entries
(transactions, balances, etc.) and anoptions_map
with metadata. All your accounts, transactions, and balances are now accessible in code. -
Leverage Beancount Query Language (BQL): Instead of manually iterating, you can run SQL-like queries on the data. For instance, to get total expenses by month, you could use the query API:
from beancount.query import query
q = query.Query(entries, options_map)
result = q.query("SELECT month, sum(position) WHERE account ~ 'Expenses' GROUP BY month")
print(result)This uses Beancount’s Query system to aggregate data. (Under the hood, this is similar to what the
bean-query
command does, but here you’re using it in a script.) In fact, the Beancount author notes that you can load the file and callrun_query()
directly via the Python API, avoiding the need to call external commands in a loop. -
Set up a project structure: Organize your scripts alongside your ledger. A common layout is to have directories for importers (to fetch/parse external data), reports or queries (for analysis scripts), and documents (to store downloaded statements). For example, one user keeps:
importers/
– custom Python import scripts (with tests),queries/
– scripts to generate reports (runnable viapython3 queries/...
),documents/
– downloaded bank CSVs/PDFs organized by account.
With this setup, you can run scripts manually (e.g. python3 queries/cash_flow.py
) or schedule them (via cron or a task runner) to automate your workflow.
Automating Reconciliation Tasks
Reconciliation means making sure your ledger matches external records (bank statements, credit card reports, etc.). Beancount’s plain-text ledger and Python API make it possible to automate much of this process.
Importing and Matching Transactions (Beginner)
For beginners, the recommended approach is to use Beancount’s importer plugins. You write a small Python class following Beancount’s importer protocol to parse a given format (CSV, OFX, PDF, etc.) and produce transactions. Then use the bean-extract
command or a script to apply these importers:
- Write an importer (a Python class with methods like
identify()
,extract()
) for your bank’s CSV format. Beancount’s documentation provides a guide and examples. - Use
bean-extract
in a script or Makefile (like thejustfile
example) to parse new statements. For instance, one workflow runsbean-extract
on all files in~/Downloads
and outputs transactions to a temporary file. - Manually review and copy transactions from the temp file into your main ledger, then run
bean-check
to ensure balances reconcile.
While this process still involves a review step, much of the grunt work of parsing and formatting entries is automated. Importer scripts can also auto-assign categories and even set balance assertions (statements of expected balances) to catch discrepancies. For example, after importing, you might have a line like 2025-04-30 balance Assets:Bank:Checking 1234.56 USD
which asserts the closing balance. When you run bean-check
, Beancount will verify that all of these balance assertions are correct, and flag any errors if transactions are missing or duplicated. This is a best practice: auto-generate balance assertions for each statement period to let the computer spot unreconciled differences for you.
Custom Reconciliation Scripts (Intermediate)
For more control, you can write a custom Python script to compare a bank’s transaction list (CSV or via API) with your ledger entries:
- Read the external data: Parse the bank’s CSV file using Python’s
csv
module (or Pandas). Normalize the data into a list of transactions, e.g. each with a date, amount, and description. - Load ledger transactions: Use
loader.load_file
as shown earlier to get all ledger entries. Filter this list to the account of interest (e.g. your checking account) and perhaps the date range of the statement. - Compare and find mismatches:
- For each external transaction, check if an identical entry exists in the ledger (match by date and amount, maybe description). If not found, mark it as “new” and possibly output it as a Beancount-formatted transaction for you to review.
- Conversely, identify any ledger entries in that account that don’t appear in the external source – these could be data entry errors or transactions that haven’t cleared the bank.
- Output results: Print a report or create a new
.beancount
snippet with the missing transactions.
As an example, a community script called reconcile.py
does exactly this: given a Beancount file and an input CSV, it prints a list of new transactions that should be imported, as well as any existing ledger postings that aren’t in the input (potentially a sign of misclassification). With such a script, monthly reconciliation can be as simple as running it and then appending the suggested transactions to your ledger. One Beancount user notes that they “do a reconciliation process on all the accounts each month” and use a growing collection of Python code to eliminate much of the manual work in importing and reconciling data.
Tip: During reconciliation, leverage Beancount’s tools for accuracy:
- Use balance assertions as mentioned, to have automated checks on account balances.
- Use the
pad
directive if desired, which can auto-insert balancing entries for minor rounding differences (use with caution). - Write unit tests for your importer or reconciliation logic (Beancount provides test helpers). For example, one workflow involved taking a sample CSV, writing failing tests with expected transactions, then implementing the importer until all tests passed. This ensures your import script works correctly for various cases.
Generating Custom Reports and Summaries
While Fava provides many standard reports (Income Statement, Balance Sheet, etc.), you can create custom reports using scripts. These can range from simple console outputs to rich formatted files or charts.
Querying Data for Reports (Beginner)
At a basic level, you can use the Beancount Query Language (BQL) to get summary data and print or save it. For example:
-
Cash Flow Summary: Use a query to compute net cash flow. “Cash flow” could be defined as the change in balance of certain accounts over a period. Using BQL, you might do:
SELECT year, month, sum(amount)
WHERE account LIKE 'Income:%' OR account LIKE 'Expenses:%'
GROUP BY year, monthThis would net all income and expense postings by month. You could run this through the
bean-query
CLI or via the Python API (query.Query
as shown earlier) and then format the result. -
Category Spending Report: Query total expenses per category:
SELECT account, round(sum(position), 2)
WHERE account ~ 'Expenses'
GROUP BY account
ORDER BY sum(position) ASCThis yields a table of expenses by category. You can run multiple queries in a script and output the results as text, CSV, or even JSON for further processing.
One user found it “trivial” to analyze financial data with Fava or with scripts, citing that they use one Python script to pull data out of Beancount via the Query Language and then put it into a Pandas DataFrame to prepare a custom report. For instance, you might fetch monthly totals with a query and then use Pandas/Matplotlib to plot a cash flow chart over time. The combination of BQL and data science libraries allows you to build reports beyond what Fava offers by default.
Advanced Reporting (Charts, Performance, etc.)
For more advanced needs, your scripts can compute metrics like investment performance or create visual outputs:
-
Investment Performance (IRR/XIRR): Since your ledger contains all cash flows (buys, sells, dividends), you can calculate portfolio return rates. For example, you could write a script that filters transactions of your investment accounts and then calculates the Internal Rate of Return. There are libraries (or formulas) to compute IRR given cash flow data. Some community-developed Fava extensions (like PortfolioSummary or fava_investor) do exactly this, computing IRR and other metrics for investment portfolios. As a script, you could use an IRR function (from NumPy or your own) on the series of contributions/withdrawals plus ending value.
-
Multi-period or Custom Metrics: Want a report of your savings rate (ratio of savings to income) each month? A Python script can load the ledger, sum up all Income accounts and all Expense accounts, then compute savings = income - expenses and the percentage. This could output a nice table or even generate an HTML/Markdown report for your records.
-
Visualization: You can generate charts outside of Fava. For example, use
matplotlib
oraltair
in a script to create a net worth over time chart, using ledger data. Because the ledger has all historic balances (or you can accumulate them by iterating entries), you can produce time series plots. Save these charts as images or interactive HTML. (If you prefer in-app visuals, see the Fava extension section below for adding charts within Fava.)
Output Options: Decide how to deliver the report:
- For one-off analysis, printing to screen or saving to a CSV/Excel file might suffice.
- For dashboards, consider generating an HTML file with the data (possibly using a templating library like Jinja2 or even just writing Markdown) that you can open in a browser.
- You can also integrate with Jupyter Notebooks for an interactive reporting environment, although that’s more for exploration than automation.
Triggering Alerts from Your Ledger
Another powerful use of scriptable workflows is setting up alerts based on conditions in your financial data. Because your ledger is updated regularly (and can include future-dated items like upcoming bills or budgets), you can scan it with a script and get notified of important events.