The 15-Minute Compliance Report: BQL Queries That Satisfy Auditors Fast

Last month, an auditor walked into my client’s office at 9 AM requesting detailed expense documentation for their Q4 review—due by end of day. Three years ago, this would have meant a frantic scramble: pulling bank statements, formatting Excel spreadsheets, cross-referencing receipts, and hoping I didn’t miss anything. The client and I would’ve worked until midnight.

This time? I opened my terminal, ran three BQL queries, and had audit-ready PDF reports in the auditor’s hands by 9:15 AM. The auditor actually asked if I’d prepared these in advance. Nope—just Beancount Query Language doing what it does best.

The Problem with Traditional Compliance Reporting

As CPAs, we know auditors need specific things: transaction details, balance verification, categorical summaries, multi-period comparisons. The information exists in our books, but extracting it traditionally means:

  • Hours of manual sorting and filtering
  • Error-prone copy-paste between systems
  • Reformatting for readability
  • Double-checking every figure
  • Praying you didn’t miss edge cases

With PCAOB AS 1215 (effective December 15, 2026) tightening documentation requirements—demanding that audit trails clearly demonstrate “who performed the work, evidence obtained, and conclusions reached”—the compliance bar keeps rising. We need systems that make thoroughness effortless, not painful.

BQL Queries That Auditors Actually Want

Here are the four query templates that have saved me dozens of hours this tax season:

1. Transaction Detail Report (Specific Account, Date Range)

SELECT 
  date, 
  narration, 
  position, 
  balance 
FROM 
  OPEN ON 2025-10-01 CLOSE ON 2025-12-31 
WHERE 
  account ~ 'Expenses:Office:Supplies'

This gives auditors the chronological transaction list they always request first. The narration field contains your receipt references, and position shows amounts with full precision.

Auditor Translation: “Show me everything that hit this account in Q4.”

2. Balance Assertions Verification

SELECT 
  account, 
  LAST(balance) AS final_balance
FROM 
  OPEN ON 2026-01-01 CLOSE ON 2026-01-31
WHERE 
  account ~ 'Assets|Liabilities'
GROUP BY 
  account

Beancount’s balance assertions are game-changers for audit compliance. This query proves that every account balance was verified against external statements throughout the period. No other system I’ve used makes this as transparent.

Auditor Translation: “Prove your books match bank statements.”

3. Expense Categorization Summary with Counts

SELECT 
  account, 
  SUM(position) AS total, 
  COUNT(*) AS transaction_count
FROM 
  OPEN ON 2025-01-01 CLOSE ON 2025-12-31
WHERE 
  account ~ 'Expenses'
GROUP BY 
  account
ORDER BY 
  total DESC

This satisfies the “show me where the money went” request. The transaction counts help auditors spot anomalies—if you have 347 office supply transactions but only 12 rent payments, they’ll want to know why.

Auditor Translation: “Categorize annual spending and prove it’s reasonable.”

4. Multi-Year Comparison (Trend Analysis)

SELECT 
  YEAR(date) AS year, 
  account, 
  SUM(position) AS annual_total
FROM 
  has_account('Expenses:Marketing')
WHERE 
  account = 'Expenses:Marketing'
GROUP BY 
  year, account

Auditors love trend analysis. Sudden spikes or drops trigger questions. This query lets you get ahead of those questions by showing year-over-year consistency (or justified variance).

Auditor Translation: “Is this year’s spending pattern consistent with history?”

Why This Satisfies PCAOB AS 1215 Standards

The updated audit documentation standards require evidence that’s detailed enough for “an experienced auditor, having no previous connection with the engagement” to understand everything. Beancount’s plain text ledger + BQL queries satisfy this beautifully:

  • Complete audit trail: Every transaction includes date, amount, accounts, and narrative description
  • Version control ready: Git commits show who made changes and when
  • Immutable by design: You can’t silently “fix” historical entries without leaving evidence
  • Judgment documentation: Comments in ledger files explain complex categorization decisions
  • Risk assessment: Metadata tags let you mark high-risk or unusual transactions

Traditional accounting software has audit trails, but they’re often opaque databases requiring specialized tools. Beancount’s plain text? An auditor can read it with Notepad if needed.

Real Impact: 3 Days → 3 Hours

For my small business clients, audit prep used to consume 2-3 days of billable time: gathering docs, formatting reports, answering follow-up questions, formatting more reports. Now it’s a half-day affair:

  • Morning: Run BQL queries, generate reports
  • Lunch: Auditor reviews, asks clarifying questions
  • Afternoon: Run 2-3 additional queries to address edge cases
  • Done.

The client saves money, I deliver faster, and auditors get exactly what they need in a format that’s actually useful (not 50-page Excel dumps).

The Git Advantage for Professional Accountability

One bonus that surprised me: auditors love that my clients’ books are in Git repositories. When they ask “when was this transaction recorded?” I can show them the commit timestamp. When they question a categorization decision, I point to the commit message explaining the reasoning.

This level of transparency builds trust. I’ve had auditors specifically note in reports that our documentation quality “exceeded requirements”—not because I’m doing extra work, but because Beancount makes thoroughness the default.

Your Turn: What Queries Do You Use?

I’ve shared my core four, but I know this community has battle-tested queries I haven’t discovered yet. What BQL queries do you run for:

  • Multi-state tax reporting?
  • Client invoicing and AR aging?
  • Payroll tax compliance?
  • Grant fund tracking (for nonprofits)?

Also curious: anyone automating this further with Python scripts? Or integrating BQL outputs with other reporting tools?

Let’s build a compliance query library together. Tax season is brutal enough—we should make the reporting part painless.

Tina, this is SO timely! I’ve been meaning to write up something similar from my practice, but you nailed it with the practical query examples.

Your point about the 1099-K threshold dropping to $5K this year is critical—I’ve had three freelance clients get surprised by forms they never received before. Having BQL queries that separate deductible vs non-deductible expenses has been a lifesaver during these conversations.

My IRS Schedule C Query Template

Since you asked about multi-state tax reporting, here’s the query I use for freelancers preparing Schedule C. It groups expenses by IRS-recognized categories:

SELECT 
  REGEXP_REPLACE(account, 'Expenses:Business:', '') AS irs_category,
  SUM(position) AS total_deductible,
  COUNT(*) AS num_transactions
FROM 
  OPEN ON 2025-01-01 CLOSE ON 2025-12-31
WHERE 
  account ~ 'Expenses:Business' 
  AND NOT account ~ 'Personal'
GROUP BY 
  irs_category
ORDER BY 
  total_deductible DESC

The key is setting up your chart of accounts to mirror IRS categories from day one. My standard structure for freelance clients:

  • Expenses:Business:Advertising
  • Expenses:Business:CarAndTruck
  • Expenses:Business:OfficeExpense
  • Expenses:Business:Travel
  • etc.

This way, the BQL output maps directly to Schedule C lines. No manual remapping needed.

Quarterly Estimated Tax Calculations

For quarterly payments (which trip up SO many freelancers), I run this on a rolling basis:

SELECT 
  QUARTER(date) AS quarter,
  SUM(CASE WHEN account ~ 'Income:Freelance' THEN position ELSE 0 END) AS gross_income,
  SUM(CASE WHEN account ~ 'Expenses:Business' THEN position ELSE 0 END) AS deductions,
  (gross_income + deductions) * 0.9235 * 0.153 AS se_tax_estimate
FROM 
  OPEN ON 2025-01-01 CLOSE ON 2025-12-31
GROUP BY 
  quarter

This gives rough self-employment tax estimates by quarter. Helps clients avoid the “oh crap, I owe $8K and have $300 in the bank” panic in April.

The Underpayment Penalty Story

Last year, a client avoided a $1,200 underpayment penalty because we had real-time BQL tracking showing their income spiked unexpectedly in Q4. We ran an emergency query, calculated safe harbor (110% of prior year for high earners), and made an estimated payment before year-end.

Without Beancount’s instant analysis, we wouldn’t have caught it until tax prep—too late.

Multi-State Question for You

You mentioned multi-state reporting in your original post. I’m working with a remote consultant who has nexus in 4 states. Do you have a BQL approach for apportioning income by state?

Right now I’m using metadata tags (state: "CA") on transactions, but I’m curious if there’s a cleaner way to handle it, especially for expense allocation across multiple states.

Also—have you experimented with Python automation for quarterly tax reminders? I’ve been thinking about building a script that sends clients alerts when their estimated tax liability crosses certain thresholds.

This thread is exactly what I needed to see today! Thank you both for sharing these practical examples.

My BQL Journey: From Intimidated to Empowered

When I first discovered Beancount three years ago, I was excited about plain text accounting but completely intimidated by BQL. I’d open the query shell, stare at the prompt, and close it. Fava’s UI was comfortable—why did I need queries?

Then last year during tax prep, my accountant asked for “a breakdown of rental property expenses by property, with monthly totals.” I spent four hours manually filtering Fava’s transaction list and copying data into a spreadsheet.

That’s when I finally forced myself to learn BQL. Turns out, it’s just SQL for accountants. If you can write a WHERE clause, you can write useful queries.

My Learning Path (For Anyone Still Intimidated)

Month 1: Only used Fava, avoided bean-query completely

Month 3: Tried my first SELECT query—just pulled all transactions for one account. Felt like magic when it worked.

Month 6: Started using GROUP BY for category summaries. Realized I could answer my own questions instead of waiting for monthly reports.

Month 12: Writing complex queries with regex filters, date ranges, and multi-condition WHERE clauses. Built a “monthly dashboard” query I run on the 1st of every month.

Now: I can’t imagine going back. Queries have become second nature.

My Rental Property Query

Since Alice asked about property-level P&L, here’s what I use for my two rental units:

SELECT 
  REGEXP_EXTRACT(account, r'Property:(.*?):') AS property_name,
  MONTH(date) AS month,
  SUM(CASE WHEN account ~ 'Income:Rental' THEN position ELSE 0 END) AS rental_income,
  SUM(CASE WHEN account ~ 'Expenses:Property' THEN position ELSE 0 END) AS expenses,
  rental_income + expenses AS net_income
FROM 
  OPEN ON 2025-01-01 CLOSE ON 2025-12-31
WHERE 
  account ~ 'Property:(OakStreet|MapleAve)'
GROUP BY 
  property_name, month
ORDER BY 
  property_name, month

My account structure: Income:Rental:Property:OakStreet and Expenses:Property:OakStreet:Maintenance, etc.

The REGEXP_EXTRACT pulls the property name dynamically, so if I buy a third property, the query automatically includes it. No manual updates.

The Python Automation Question

Alice, you asked about Python automation—YES! I’ve been doing this for about 6 months now.

My setup:

  • Monthly cron job (runs on the 1st at 8 AM)
  • Python script calls bean-query via subprocess
  • Executes 5 different BQL queries (net worth, category spending, property P&L, investment performance, upcoming bills)
  • Generates a PDF using ReportLab
  • Emails it to me and my spouse

The whole thing runs in under 30 seconds. We review it over coffee on the 1st of every month, and it’s become our “monthly financial check-in” ritual.

I won’t paste the full script here (it’s ~200 lines), but the core pattern is:

import subprocess
import pandas as pd

def run_bql_query(ledger_file, query):
    result = subprocess.run(
        ['bean-query', ledger_file, query],
        capture_output=True,
        text=True
    )
    # Parse output into pandas DataFrame
    # Generate charts/tables
    return result.stdout

My “Starter Queries” Resource

For anyone just getting into BQL, I maintain a personal “starter queries” document with templates for common tasks:

  1. All transactions for an account in a date range
  2. Monthly spending by category
  3. Net worth over time
  4. Account balance on a specific date
  5. Year-over-year expense comparison

These five queries cover 80% of what most people need. Once you’ve mastered these, the advanced stuff (like Tina’s self-employment tax calculator) starts making sense.

Encouragement for Beginners

If you’re reading this and thinking “I could never learn SQL,” I felt the same way. Start with the simplest possible query:

SELECT * FROM has_account('Assets:Checking')

Run it. See what happens. Then add a WHERE clause. Then a date range. Build complexity gradually.

The “aha moment” for me was realizing: I’m asking my financial data questions, and it’s answering me. That’s incredibly powerful.

Alice and Tina—do either of you have thoughts on BQL query optimization? I’ve noticed some of my queries take 5-10 seconds on large ledgers. Are there best practices for performance?

Reading this thread as a small bookkeeping practice owner, I’m nodding along to every single point. BQL has completely transformed how I interact with clients—and more importantly, how they perceive the value I provide.

The “I’ll Get Back to You” Problem

Before Beancount, client requests went like this:

Client: “Can you send me a breakdown of Q3 marketing spend by campaign?”
Me: “Sure, I’ll pull that together and email it by end of week.”
Reality: I’d spend 2-3 hours on Friday afternoon manually sorting transactions, double-checking categorization, formatting a spreadsheet, and writing an email explanation.

The client got their answer eventually, but the delay positioned me as a service provider rather than a strategic advisor.

The BQL Transformation

Now the same conversation goes:

Client: “Can you send me Q3 marketing spend by campaign?”
Me: “Give me 90 seconds…” (runs BQL query, exports to PDF) “…just emailed it to you. The $12K spike in August was the Google Ads campaign we discussed.”
Client: “Wait, you had that ready?”
Me: “I can pull any financial view you need, anytime.”

That perception shift is everything. I went from “the person who handles my books” to “the person who knows my business finances better than I do.”

The Report Library Approach

I’ve built what I call my “Report Library”—a collection of 15-20 pre-written BQL queries for common client requests:

  • Monthly P&L by department
  • Vendor spending ranked by total
  • Revenue by product line
  • Cash flow projection (90 days forward)
  • Customer AR aging
  • Expense trend analysis (this year vs last year)

When a client asks for something, I check my library first. 90% of the time, I already have the query. I just update the date range and run it.

For the other 10%, I write a new query and add it to the library for next time.

The Board Meeting Game-Changer

One of my nonprofit clients has monthly board meetings. The executive director used to scramble the day before, asking for financial snapshots, grant fund balances, program-level spending, etc.

Now I have a “board meeting package” query set:

  1. Overall financial position: Assets, liabilities, net position
  2. Grant tracking: Restricted vs unrestricted fund balances by grant
  3. Program expenses: Spending by program, compared to budget
  4. Cash runway: Current cash ÷ average monthly burn rate

I run all four queries at once (takes maybe 3 minutes total), compile them into a PDF report, and send it over. The ED forwarded it to the board treasurer, who replied: “This is the clearest financial reporting we’ve ever had.”

That kind of feedback leads to referrals. Three of my current clients came from that nonprofit’s board members.

The Pricing Impact

Here’s the business model shift that surprised me: I can now justify premium pricing for what I call my “Real-Time Financial Analytics” package.

Standard Package: Monthly books, basic P&L and balance sheet ($500/month)

Analytics Package: Everything in Standard, plus unlimited BQL query requests via Slack, custom monthly reports, quarterly strategic reviews ($1,200/month)

The only difference in my actual work? I run BQL queries on demand instead of manually building spreadsheets. But clients perceive it as a premium service because they get answers in minutes instead of days.

Six of my 40 clients have upgraded to the Analytics package. That’s an extra $4,200/month in revenue for work that takes me maybe 2-3 extra hours per client per month.

The Billable Hour Tension

Alice asked earlier about balancing automation with the billable hour model. I’ve struggled with this too.

My solution: I don’t bill by the hour anymore for most clients. I shifted to fixed monthly retainers based on complexity and value delivered, not time spent.

This aligns incentives perfectly. The more I automate with BQL:

  • Clients get faster, better answers (they’re happy)
  • I spend less time on routine reporting (I’m happy)
  • I can take on more clients without hiring (I’m profitable)

The old model punished efficiency. The new model rewards it.

My Most-Used Query: AR Aging

Since people are sharing specific queries, here’s the one I run most often for small business clients—accounts receivable aging:

SELECT 
  narration AS invoice_description,
  date AS invoice_date,
  DAYS(TODAY(), date) AS days_outstanding,
  position AS amount_due,
  CASE 
    WHEN DAYS(TODAY(), date) <= 30 THEN 'Current'
    WHEN DAYS(TODAY(), date) <= 60 THEN '30-60 days'
    WHEN DAYS(TODAY(), date) <= 90 THEN '60-90 days'
    ELSE 'Over 90 days'
  END AS aging_bucket
FROM 
  has_account('Assets:AccountsReceivable')
WHERE 
  account = 'Assets:AccountsReceivable'
  AND position != 0
ORDER BY 
  days_outstanding DESC

Clients love seeing exactly which invoices are overdue and by how long. It’s actionable intelligence, not just historical reporting.

Question for the Group

For those of you with clients who want direct access to their books—have you set up any kind of collaborative workflow where clients can run queries themselves?

I’ve been hesitant to give clients raw ledger access (worried they’ll break something), but I also think teaching them basic BQL could be empowering. Curious if anyone has tried this and how it went.

Mike (helpful_veteran), I’m so glad you brought up Python automation! I’ve been running automated BQL reports for about 18 months now, and it’s been a game-changer for my personal finance workflow.

My Automation Stack

Here’s my full setup for anyone interested:

Infrastructure:

  • Raspberry Pi running 24/7 (overkill, but I had one lying around)
  • Cron job: 0 8 1 * * /home/fred/beancount/monthly_report.sh
  • Python 3.11 with pandas, matplotlib, reportlab, smtplib

What It Does:

  1. Runs 6 different BQL queries against my ledger
  2. Generates charts (net worth trend, category spending pie chart, investment allocation)
  3. Compiles everything into a PDF report
  4. Emails it to me and my spouse on the 1st of every month at 8 AM

Total execution time: ~25 seconds for a 4-year ledger with 12,000+ transactions.

The Core Script Pattern

Here’s the skeleton of how I execute BQL queries from Python:

import subprocess
import pandas as pd
from io import StringIO

def run_bql_query(ledger_path, query):
    """Execute a BQL query and return results as pandas DataFrame"""
    cmd = ['bean-query', ledger_path, query]
    result = subprocess.run(cmd, capture_output=True, text=True)
    
    if result.returncode != 0:
        raise Exception(f"Query failed: {result.stderr}")
    
    # bean-query outputs space-separated tabular data
    # Parse it into a DataFrame
    df = pd.read_csv(StringIO(result.stdout), delim_whitespace=True)
    return df

# Example usage:
query = """
SELECT 
  YEAR(date) AS year, 
  MONTH(date) AS month,
  SUM(position) AS total
FROM 
  has_account('Expenses')
WHERE 
  account ~ 'Expenses'
GROUP BY 
  year, month
"""

df = run_bql_query('/home/fred/finances/ledger.beancount', query)
print(df)

The key insight: bean-query outputs tab/space-separated data that pandas can parse directly. No manual CSV exports needed.

The Reports I Generate

1. Net Worth Trend (12-Month Chart)

query = """
SELECT 
  MONTH(date) AS month,
  SUM(CONVERT(position, 'USD')) AS net_worth
FROM 
  OPEN ON 2025-01-01 CLOSE ON 2025-12-31
WHERE 
  account ~ 'Assets|Liabilities'
GROUP BY 
  month
"""
df = run_bql_query(ledger, query)

import matplotlib.pyplot as plt
plt.plot(df['month'], df['net_worth'])
plt.title('Net Worth Trend - Last 12 Months')
plt.savefig('net_worth.png')

2. Category Spending vs Budget

I track budgets using metadata in my ledger. Then I compare actual spending to budget targets:

query = """
SELECT 
  account,
  SUM(position) AS actual,
  -1 * MAX(LAST(balance)) AS budget,
  actual - budget AS variance
FROM 
  OPEN ON 2026-03-01 CLOSE ON 2026-03-31
WHERE 
  account ~ 'Expenses'
GROUP BY 
  account
ORDER BY 
  variance DESC
"""

This instantly shows which categories I’m over/under budget. I include a “variance %” column too.

3. Tax Planning Estimates

Since I’m self-employed, I run quarterly projections for estimated taxes:

query = """
SELECT 
  QUARTER(date) AS quarter,
  SUM(CASE WHEN account ~ 'Income:Freelance' THEN position ELSE 0 END) * -1 AS income,
  SUM(CASE WHEN account ~ 'Expenses:Business' THEN position ELSE 0 END) AS deductions,
  (income - deductions) * 0.30 AS estimated_tax
FROM 
  OPEN ON 2026-01-01 CLOSE ON TODAY()
GROUP BY 
  quarter
"""

I have the script send me a Slack notification if my Q1 tax estimate exceeds $5K (my threshold for making an estimated payment).

4. FIRE Progress Tracking

I’m pursuing financial independence, so I track my savings rate and “years to FI”:

query = """
SELECT 
  SUM(CASE WHEN account ~ 'Income' THEN position ELSE 0 END) * -1 AS total_income,
  SUM(CASE WHEN account ~ 'Expenses' THEN position ELSE 0 END) AS total_expenses,
  (total_income - total_expenses) / total_income AS savings_rate,
  CONVERT(SUM(CASE WHEN account ~ 'Assets:Investments' THEN position ELSE 0 END), 'USD') AS invested_assets
FROM 
  OPEN ON 2026-01-01 CLOSE ON TODAY()
"""

Based on savings rate and current assets, I calculate estimated years to FI using the standard formula. Seeing that number tick down every month is incredibly motivating.

Warnings: Don’t Over-Engineer Early

Mike, you mentioned you’re just getting started with queries. My advice: don’t build automation too soon.

I spent my first 6 months just learning BQL and manually running queries. Only after I found myself running the same queries every month did I automate.

If I’d tried to automate on day 1, I would’ve built the wrong thing and gotten frustrated. Let your workflow stabilize first, then automate the repetitive parts.

The Slack Bot Experiment

You asked about Slack integration—yes! I built a simple bot that lets me ask financial questions via Slack:

Me: /finance spending-last-month Dining
Bot: You spent $287 on Dining last month (23 transactions)

It’s just a webhook that:

  1. Parses the Slack command
  2. Constructs a BQL query dynamically
  3. Runs it via subprocess
  4. Sends the result back to Slack

Honestly, it’s more of a party trick than a productivity tool. I rarely use it. But it’s fun to show people.

Performance Optimization (Answering Mike’s Question)

You mentioned some queries take 5-10 seconds. A few optimization tips:

  1. Narrow date ranges: OPEN ON ... CLOSE ON is much faster than querying the entire ledger history
  2. Use specific account filters: WHERE account = 'Expenses:Groceries' beats WHERE account ~ 'Expenses'
  3. Limit result sets: Add LIMIT 100 if you only need top results
  4. Precompile large ledgers: I keep a “current year only” ledger file for fast queries, and a “full history” file for annual analysis

My typical query on a 12K transaction ledger runs in under 1 second. If you’re seeing 5-10 seconds, try narrowing your scope.

Code Sharing

Mike and Alice both asked about sharing code. I’ve been meaning to clean up my scripts and put them on GitHub. If there’s interest, I can create a “beancount-automation-examples” repo with:

  • Monthly report generator (the script I described above)
  • Slack bot webhook handler
  • Tax estimate calculator
  • FIRE progress tracker

Would that be useful to folks? Or is everyone’s workflow too custom for shared code to matter?