The State of Beancount Plugins in 2025
The Beancount plugin ecosystem has matured significantly over the past few years. Martin Blais (Beancount’s creator) has explicitly stated that more functionality is moving to plugins rather than being built into the core, making the plugin system increasingly important for power users.
Let me walk through the essential plugins, the import automation landscape, and how to write your own custom plugins.
Essential Plugins You Should Know
1. beancount.plugins.check.check_zero_sum
What it does: Ensures specific accounts sum to zero over a period (perfect for budgets and tracking categories).
Use case: Tracking reimbursable expenses
plugin "beancount.plugins.check.check_zero_sum" "Expenses:Reimbursable"
2024-01-15 * "Business lunch (will be reimbursed)"
Expenses:Reimbursable:Meals 50.00 USD
Assets:Cash -50.00 USD
2024-01-30 * "Reimbursement received"
Assets:Bank:Checking 50.00 USD
Expenses:Reimbursable:Meals -50.00 USD
; At period end, Expenses:Reimbursable should sum to zero
; If it doesn't, you forgot to get reimbursed!
2. beancount_reds_plugins.capital_gains_classifier
What it does: Automatically classifies capital gains as short-term or long-term for tax purposes.
Installation: pip install beancount-reds-plugins
plugin "beancount_reds_plugins.capital_gains_classifier" "{
'long_term_account': 'Income:CapitalGains:LongTerm',
'short_term_account': 'Income:CapitalGains:ShortTerm'
}"
; When you sell stock, it automatically routes gains to the right account
; based on holding period (>365 days = long-term)
3. beancount_interpolate
What it does: Splits recurring transactions across time periods automatically.
Use case: Annual subscriptions that should be allocated monthly
plugin "beancount_interpolate.recur"
2024-01-01 * "Annual software subscription" ^recur-monthly-12
Expenses:Software:Subscription 1200.00 USD
Assets:Cash -1200.00 USD
; Plugin automatically creates 12 monthly transactions of $100 each
4. beancount_portfolio_allocation
What it does: Validates that your investment portfolio matches target allocations.
plugin "beancount_portfolio_allocation" "{
'target': {
'Stocks': 0.70,
'Bonds': 0.20,
'Cash': 0.10
},
'tolerance': 0.05
}"
; Warns you when allocations drift beyond 5% tolerance
5. beancount-plugins: gains_minimizer
What it does: Automatically selects which tax lots to sell to minimize capital gains.
Use case: Tax-loss harvesting and optimizing sales
plugin "beancount.plugins.unrealized" "Unrealized"
2024-06-01 * "Sell 10 shares (plugin picks best lots)"
Assets:Investments:AAPL -10 AAPL {} @ 180.00 USD
Assets:Cash 1800.00 USD
Income:Gains
; Plugin automatically fills in {} with optimal lot selection
The Import Automation Landscape
beancount-import: The Semi-Automated Approach
GitHub: GitHub - jbms/beancount-import: Web UI for semi-automatically importing external data into beancount
This is the most mature import solution. It’s semi-automated, meaning it presents candidates for you to review:
pip install beancount-import
python -m beancount_import.webserver --data_dir ~/finances/import-data --ledger ~/finances/main.beancount
Key features:
- Web UI for reviewing imported transactions
- Fuzzy matching against existing payees/accounts
- Learns from your corrections
- Handles CSV, OFX, and QFX formats
- Deduplication across sources
Configuration example (import_config.py):
from beancount_import.source import ofx, csv
sources = [
ofx.OfxSource(
account='Assets:Bank:Checking',
ofx_dir='~/downloads/bank',
),
csv.CsvSource(
account='Liabilities:CreditCard:Visa',
csv_filename='~/downloads/visa.csv',
field_mappings={
'date': 'Date',
'payee': 'Description',
'amount': 'Amount',
}
),
]
smart_importer: ML-Powered Categorization
GitHub: GitHub - beancount/smart_importer: Augment Beancount importers with machine learning functionality.
Uses machine learning to predict categories based on your historical data:
from beancount.ingest import extract
from smart_importer import apply_hooks, PredictPayees, PredictPostings
CONFIG = [
apply_hooks(MyBankImporter(), [
PredictPayees(training_data='main.beancount'),
PredictPostings(training_data='main.beancount'),
])
]
After training on your ledger, it can accurately predict:
- Payee names (standardizing “AMZN MKTP” → “Amazon”)
- Expense categories (learns that “Whole Foods” → “Expenses:Groceries”)
bean-identify and bean-extract
Built-in tools for the classic Beancount import workflow:
# 1. Identify file types
bean-identify import-config.py ~/downloads/*.csv
# 2. Extract transactions
bean-extract import-config.py ~/downloads/bank_statement.csv > to_review.beancount
# 3. Review and merge manually
cat to_review.beancount >> main.beancount
Integration with Bank APIs
plaid2text: Modern Bank API Integration
Uses Plaid API for direct bank connectivity:
pip install plaid2text
plaid2text --account "Assets:Bank:Checking" --days 30 --output new_transactions.beancount
Pros: Real-time data, no manual downloads
Cons: Requires Plaid API key, not all banks supported
Banking APIs in 2025
The landscape is improving:
- US: Plaid, Yodlee, Finicity (most major banks)
- EU: Open Banking APIs (PSD2 compliance)
- UK: TrueLayer, Token.io
Most require developer accounts but offer free tiers for personal use.
Writing Custom Plugins
The Plugin Hook System
Beancount plugins use a simple hook architecture. Every plugin implements:
from beancount.core import data
__plugins__ = ('my_plugin',)
def my_plugin(entries, options_map, config):
"""
Args:
entries: List of directives (transactions, balances, etc.)
options_map: Dict of options from the ledger file
config: String configuration passed to plugin
Returns:
(modified_entries, errors)
"""
errors = []
new_entries = []
for entry in entries:
# Process each directive
if isinstance(entry, data.Transaction):
# Do something with transactions
pass
new_entries.append(entry)
return new_entries, errors
Example: Auto-Split Plugin
Let’s write a plugin that automatically splits transactions tagged with #split:
# save as: my_plugins/auto_split.py
from beancount.core import data, amount
from beancount.core.data import Transaction, Posting
from decimal import Decimal
__plugins__ = ('auto_split',)
def auto_split(entries, options_map, config):
"""Auto-split transactions tagged with #split"""
errors = []
new_entries = []
split_account = config or "Expenses:Shared:Receivable"
for entry in entries:
if isinstance(entry, Transaction) and 'split' in entry.tags:
# Find the expense posting
expense_posting = None
for posting in entry.postings:
if posting.account.startswith('Expenses:'):
expense_posting = posting
break
if expense_posting:
# Split 50/50
original_amount = expense_posting.units
half_amount = amount.Amount(
original_amount.number / Decimal('2'),
original_amount.currency
)
# Modify postings
modified_postings = []
for posting in entry.postings:
if posting is expense_posting:
# Reduce expense by half
modified_postings.append(
posting._replace(units=half_amount)
)
# Add receivable for other half
modified_postings.append(
Posting(
account=split_account,
units=half_amount,
cost=None,
price=None,
flag=None,
meta={}
)
)
else:
modified_postings.append(posting)
# Create new transaction with modified postings
entry = entry._replace(postings=modified_postings)
new_entries.append(entry)
return new_entries, errors
Usage:
plugin "my_plugins.auto_split" "Expenses:Shared:Receivable"
2024-01-15 * "Dinner with roommate" #split
Expenses:Dining 80.00 USD
Assets:Cash -80.00 USD
; Plugin transforms to:
; Expenses:Dining 40.00 USD
; Expenses:Shared:Receivable 40.00 USD
; Assets:Cash -80.00 USD
Import Best Practices
1. Deduplication Strategy
Use transaction IDs in metadata:
2024-01-15 * "Grocery shopping"
transaction_id: "BANK_12345678"
Expenses:Groceries 50.00 USD
Assets:Bank:Checking
Your importer can check for existing IDs before importing.
2. Categorization Workflow
Three-tier approach:
- Automatic: smart_importer handles 70-80% with confidence
- Review queue: Low-confidence predictions for manual review
- Manual entry: Complex transactions (splits, investments)
3. Use Importers as Libraries
Don’t just extract to files—build interactive tools:
from my_importers import BankImporter
importer = BankImporter()
transactions = importer.extract('statement.csv')
for txn in transactions:
print(f"{txn.date} {txn.payee} {txn.amount}")
category = input("Category? ")
# Add to ledger with chosen category
The Future: Plugin Marketplace?
Martin Blais has mentioned interest in:
- Plugin registry: Centralized discovery
- Extensible syntax: Custom directives via plugins
- Better testing tools: Plugin test framework
- Performance improvements: Lazy loading, caching
The community is also working on:
- Web-based import UI: beancount-import successor
- Mobile apps: Expense capture on-the-go
- Receipt OCR: Automatic extraction from photos
Essential Resources
- Awesome Beancount: GitHub - wzyboy/awesome-beancount: Beancount tips and tricks (comprehensive plugin list)
- Plugin API docs: Beancount Scripting Plugins - Beancount Documentation
- Ingest framework: Importing External Data - Beancount Documentation
What plugins do you use? Any custom ones you’ve written that might be useful for others?