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.
Low Account Balance Warnings
To avoid overdrafts or to maintain a minimum balance, you might want an alert if any account (e.g. checking or savings) falls below a threshold. Here’s how you can implement this:
-
Determine current balances: After loading
entries
via the loader, calculate the latest balance of the accounts of interest. You can do this by aggregating postings or using a query. For example, use a BQL query for a specific account’s balance:SELECT sum(position) WHERE account = 'Assets:Bank:Checking'
This returns the current balance of that account (sum of all its postings). Alternatively, use Beancount’s internal functions to build a balance sheet. For instance:
from beancount.core import realization
tree = realization.realize(entries, options_map)
acct = realization.get_or_create(tree, "Assets:Bank:Checking")
balance = acct.balance # an Inventory of commoditiesThen extract the numerical value (e.g.
balance.get_currency_units('USD')
might give the Decimal). However, using the query is simpler for most cases. -
Check threshold: Compare the balance to your predefined limit. If below, trigger an alert.
-
Trigger notification: This could be as simple as printing a warning to console, but for real alerts you might send an email or push notification. You can integrate with email (via
smtplib
) or a service like IFTTT or Slack’s webhook API to push the alert. For example:if balance < 1000:
send_email("Low balance alert", f"Account XYZ balance is {balance}")(Implement
send_email
with your email server details.)
By running this script daily (via a cron job or Windows Task Scheduler), you’ll get proactive warnings. Because it’s using the ledger, it can consider all transactions including ones you just added.
Upcoming Payment Deadlines
If you use Beancount to track bills or deadlines, you can mark future payments and have scripts remind you. Two ways to represent upcoming obligations in Beancount:
-
Events: Beancount supports an
event
directive for arbitrary dated notes. For example:2025-05-10 event "BillDue" "Mortgage payment due"
This doesn’t affect balances but records a date with a label. A script can scan
entries
forEvent
entries whereEvent.type == "BillDue"
(or any custom type you choose) and check if the date is within, say, the next 7 days from today. If yes, trigger an alert (email, notification, or even a popup). -
Future Transactions: Some people enter future-dated transactions (post-dated) for things like scheduled payments. These won’t show up in balances until the date passes (unless you run reports as-of future dates). A script can look for transactions dated in the near future and list them.
Using these, you could create a “tickler” script that, when run, outputs a list of tasks or bills due soon. Integrate with an API like Google Calendar or a task manager if you want to automatically create reminders there.
Anomaly Detection
Beyond known thresholds or dates, you can script custom alerts for unusual patterns. For instance, if a normally monthly expense hasn’t occurred (maybe you forgot to pay a bill), or if a category’s spending is abnormally high this month, your script could flag it. This typically involves querying recent data and comparing to history (which might be an advanced topic – possibly employing statistics or ML).
In practice, many users rely on reconciliation to catch anomalies (unexpected transactions). If you receive bank notifications (like emails for each transaction), you could parse those with a script and automatically add them to Beancount, or at least verify they’re recorded. One enthusiast even configured their bank to send transaction alert emails, with the plan to parse and append them to the ledger automatically. This kind of event-driven alert can ensure no transaction goes unrecorded.
Extending Fava with Custom Plugins and Views
Fava is already scriptable through its extension system. If you want your automation or reports to integrate directly into the web interface, you can write a Fava extension (also called a plugin) in Python.
How Fava Extensions Work: An extension is a Python module that defines a class inheriting from fava.ext.FavaExtensionBase
. You register it in your Beancount file via a custom option. For example, if you have a file myextension.py
with a class MyAlerts(FavaExtensionBase)
, you can enable it by adding to your ledger:
1970-01-01 custom "fava-extension" "myextension"
When Fava loads, it will import that module and initialize your MyAlerts
class.
Extensions can do several things:
- Hooks: They can hook into events in Fava’s lifecycle. For instance,
after_load_file()
is called after the ledger is loaded. You could use this to run checks or precompute data. If you wanted to implement the low-balance check inside Fava,after_load_file
could iterate over account balances and perhaps store warnings (though surfacing them to the UI might require a bit more work, like raising a FavaAPIError or using Javascript to show a notification). - Custom Reports/Pages: If your extension class sets a
report_title
attribute, Fava will add a new page in the sidebar for it. You then provide a template (HTML/Jinja2) for the content of that page. This is how you create entirely new views, such as a dashboard or summary that Fava doesn’t have by default. The extension can gather whatever data it needs (you can accessself.ledger
which has all entries, balances, etc.) and then render the template.
For example, the built-in portfolio_list
extension in Fava adds a page listing your portfolio positions. Community extensions go further:
- Dashboards: The fava-dashboards plugin allows defining custom charts and panels (using libraries like Apache ECharts). It reads a YAML config of queries to run, executes them via Beancount, and generates a dynamic dashboard page in Fava. In essence, it ties together Beancount data and a JavaScript charting library to produce interactive visualizations.
- Portfolio analysis: The PortfolioSummary extension (user-contributed) computes investment summaries (grouping accounts, calculating IRR, etc.) and displays them in Fava’s UI.
- Transaction review: Another extension, fava-review, helps review transactions over time (e.g. to ensure you didn’t miss any receipts).
To create a simple extension yourself, start by subclassing FavaExtensionBase
. For example, a minimal extension that adds a page could look like:
from fava.ext import FavaExtensionBase
class HelloReport(FavaExtensionBase):
report_title = "Hello World"
def __init__(self, ledger, config):
super().__init__(ledger, config)
# any initialization, perhaps parse config if provided
def after_load_file(self):
# (optional) run after ledger is loaded
print("Ledger loaded with", len(self.ledger.entries), "entries")
If you placed this in hello.py
and added custom "fava-extension" "hello"
to your ledger, Fava would show a new "Hello World" page (you’d also need a template file HelloReport.html
in a templates
subfolder to define the page content, unless the extension only uses hooks). The template can use data you attach to the extension class. Fava uses Jinja2 templates, so you might render your data into an HTML table or chart in that template.
Note: Fava’s extension system is powerful but considered “unstable” (subject to change). It requires some familiarity with web development (HTML/JS) if you’re making custom pages. If your goal is simply to run scripts or analyses, it might be easier to keep them as external scripts. Use Fava extensions when you want a tailored in-app experience for your workflow.
Integrating Third-Party APIs and Data
One of the advantages of scriptable workflows is the ability to pull in outside data. Here are common integrations:
-
Exchange Rates & Commodities: Beancount doesn’t auto-fetch prices by design (to keep reports deterministic), but it provides a Price directive for you to supply rates. You can automate fetching these prices. For example, a script can query an API (Yahoo Finance, Alpha Vantage, etc.) for the latest exchange rate or stock price and append a price entry to your ledger:
2025-04-30 price BTC 30000 USD
2025-04-30 price EUR 1.10 USDThere are tools like
bean-price
(now an external tool under beancount’s umbrella) which fetch daily quotes and output them in Beancount format. You could schedulebean-price
to run each night to update aprices.beancount
include file. Or use Python: e.g., with therequests
library to call an API. Beancount’s documentation suggests that for publicly traded assets, you can “invoke some code that will download prices and write out the directives for you.” In other words, let a script do the lookup and insert theprice
lines, rather than you doing it manually. -
Stock Portfolio Data: Similar to exchange rates, you can integrate with APIs to fetch detailed stock data or dividends. For instance, the Yahoo Finance API (or community libraries like
yfinance
) can retrieve historical data for a ticker. A script might update your ledger with monthly price history for each stock you own, enabling accurate historical reports of market value. Some custom extensions (like fava_investor) even pull price data on the fly for display, but simplest is to regularly import prices into the ledger. -
Banking APIs (Open Banking/Plaid): Instead of downloading CSVs, you can use APIs to fetch transactions automatically. Services like Plaid aggregate bank accounts and allow programmatic access to transactions. In an advanced setup, you could have a Python script that uses Plaid’s API to pull new transactions daily and save them to a file (or directly import to the ledger). One power-user built a system where Plaid feeds into their import pipeline, making their books nearly automatic. They note that “nothing stops you from signing up with Plaid API and doing the same locally” – i.e., you can write a local script to get bank data, then use your Beancount importer logic to parse it into ledger entries. Some regions have open banking APIs provided by banks; those could be used similarly.
-
Other APIs: You might integrate budgeting tools (exporting planned budgets to compare with actuals in Beancount), or use an OCR API to read receipts and auto-match them to transactions. Because your scripts have full access to Python’s ecosystem, you can integrate everything from email services (for sending alerts) to Google Sheets (e.g. update a sheet with monthly finance metrics) to messaging apps (send yourself a summary report via Telegram bot).
When using third-party APIs, remember to secure your credentials (use environment variables or config files for API keys), and handle errors (network issues, API downtime) gracefully in your scripts. It’s often wise to cache data (for example, store fetched exchange rates so you don’t request the same historical rate repeatedly).
Best Practices for Modular, Maintainable Scripts
As you build out scriptable workflows, keep your code organized and robust:
-
Modularity: Split different concerns into different scripts or modules. For example, have separate scripts for “data import/reconciliation” vs. “report generation” vs. “alerts”. You can even create a small Python package for your ledger with modules like
ledger_import.py
,ledger_reports.py
, etc. This makes each part easier to understand and test. -
Configuration: Avoid hard-coding values. Use a config file or top-of-script variables for things like account names, thresholds, API keys, date ranges, etc. This makes it easy to adjust without editing code deeply. For instance, define
LOW_BALANCE_THRESHOLDS = {"Assets:Bank:Checking": 500, "Assets:Savings": 1000}
at the top, and your alert script can loop through this dict. -
Testing: Treat your financial automation as mission-critical code – because it is! Write tests for complex logic. Beancount provides some test helpers (used internally for importer testing) that you can leverage to simulate ledger inputs. Even without fancy frameworks, you can have a dummy CSV and expected output transactions, and assert your import script produces the correct entries. If you use
pytest
, you can integrate these tests easily (as Alex Watt did via ajust test
command wrapping pytest). -
Version Control: Keep your ledger and scripts under version control (git). This not only gives you backups and history, but encourages you to make changes in a controlled way. You can tag releases of your “finance scripts” or review differences when debugging an issue. Some users even track their financial records in Git to see changes over time. Just be careful to ignore sensitive data (like raw statement files or API keys) in your repo.
-
Documentation: Document your custom workflows for future you. A README in your repository explaining how to set up the environment, how to run each script, and what each does will be invaluable after months have passed. Also comment your code, especially any non-obvious accounting logic or API interaction.
-
Fava Plugins Maintenance: If you write a Fava extension, keep it simple. Fava might change, so smaller extensions with targeted functionality are easier to update. Avoid duplicating too much logic – use Beancount’s query engine or existing helper functions whenever possible, rather than hardcoding calculations that might be sensitive to ledger changes.
-
Security: Since your scripts may handle sensitive data and connect to external services, treat them with care. Do not expose API keys, and consider running your automation on a secure machine. If you use a hosted solution or cloud (like scheduling GitHub Actions or a server to run Fava), ensure your ledger data is encrypted at rest and that you’re comfortable with the privacy implications.
By following these practices, you ensure that your workflow remains reliable even as your finances (and the tools themselves) evolve. You want scripts that you can reuse year after year, with minimal tweaks.
Conclusion
Beancount and Fava provide a powerful, flexible platform for tech-savvy users to completely customize their personal finance tracking. By writing Python scripts, you can automate tedious tasks like reconciling statements, produce rich reports tailored to your needs, and stay on top of your finances with timely alerts. We covered a range of examples from basic to advanced – starting with simple queries and CSV imports, and moving to full-fledged Fava plugins and external API integrations. As you implement these, start simple and gradually build up. Even a few small automation scripts can save hours of work and vastly improve accuracy. And remember, because everything is plain text and Python, you are in full control – your finance system grows with you, bending to your specific needs. Happy scripting!
Sources: The techniques above are drawn from the Beancount documentation and community experiences. For further reading, see Beancount’s official docs, community guides and blogs, and the Awesome Beancount repository for links to useful plugins and tools.