Nonprofit Peer Benchmark - Instant Demo
A board-ready peer benchmark: a nonprofit's books against similar organizations' public Form 990 filings - margin, reliance on donations, staff cost, officer pay - with every peer linked to its own filing.
8 steps · shared by Klajdi · September 20, 2026
No data is shared in this template. It contains only the recipe — column names and SQL logic. When you run it, your data is processed in your own browser and never leaves your machine.
What data it expects
NONPROFIT_PeerFilings.csv
role · VARCHARorganization · VARCHARfiscal_year · BIGINT<one row per organization per fiscal year - revenue, expenses, assets, net assets, filing_url> · DOUBLE
QBO_ProfitAndLoss_Month.csv
section · VARCHARaccount · VARCHAR<one column per month, YYYY-MM> · DOUBLE
Connect QuickBooks Online or drop a CSV / Excel export with a similar layout — the AI adapts the workflow if your columns differ.
How it works — every step, readable
- 01Peer filings - Michigan humane societies (live, public Form 990s)
SELECT * FROM read_csv_auto('NONPROFIT_PeerFilings.csv', header=true, types={'ein': 'VARCHAR'}) - 02Demo shelter - last 12 months of books (sample)
SELECT section, account, CAST(COLUMNS(* EXCLUDE (section, account)) AS DOUBLE) FROM (VALUES ('Income', 'Donations & contributions', 93100.0, 97020.0, 100940.0, 94080.0, 117600.0, 101920.0, 95060.0, 98980.0, 102900.0, 96040.0, 149940.0, 214130.0), ('Income', 'Grants', 19950.0, 20790.0, 56238.0, 20160.0, 21000.0, 21840.0, 20370.0, 21210.0, 48510.0, 20580.0, 21420.0, 19950.0), ('Income', 'Adoption fees', 13775.0, 14355.0, 14935.0, 13920.0, 14500.0, 19604.0, 18987.75, 18306.25, 15225.0, 14210.0, 14790.0, 13775.0), ('Income', 'Clinic & program service fees', 11210.0, 11682.0, 12154.0, 11328.0, 11800.0, 12272.0, 11446.0, 11918.0, 12390.0, 11564.0, 12036.0, 11210.0), ('Income', 'Special events', 5700.0, 5940.0, 6180.0, 17280.0, 6000.0, 6240.0, 5820.0, 6060.0, 6300.0, 32340.0, 6120.0, 5700.0), ('Other Income', 'Investment income', 1805.0, 1881.0, 1957.0, 1824.0, 1900.0, 1976.0, 1843.0, 1919.0, 1995.0, 1862.0, 1938.0, 1805.0), ('Expenses', 'Salaries & wages', 83600.0, 87120.0, 90640.0, 84480.0, 88000.0, 91520.0, 85360.0, 88880.0, 92400.0, 86240.0, 89760.0, 83600.0), ('Expenses', 'Executive Director salary', 9120.0, 9504.0, 9888.0, 9216.0, 9600.0, 9984.0, 9312.0, 9696.0, 10080.0, 9408.0, 9792.0, 9120.0), ('Expenses', 'Payroll taxes & benefits', 16910.0, 17622.0, 18334.0, 17088.0, 17800.0, 18512.0, 17266.0, 17978.0, 18690.0, 17444.0, 18156.0, 16910.0), ('Expenses', 'Animal care & veterinary supplies', 25175.0, 26235.0, 27295.0, 25440.0, 26500.0, 33072.0, 32131.25, 26765.0, 27825.0, 25970.0, 27030.0, 25175.0), ('Expenses', 'Occupancy & utilities', 14725.0, 14731.2, 12772.0, 11904.0, 12400.0, 12896.0, 12028.0, 12524.0, 13020.0, 12152.0, 12648.0, 14136.0), ('Expenses', 'Fundraising events & mailings', 6840.0, 7128.0, 7416.0, 6912.0, 7200.0, 7488.0, 6984.0, 7272.0, 7560.0, 22579.2, 11750.4, 6840.0), ('Expenses', 'Professional fees', 4085.0, 4257.0, 4429.0, 9907.2, 4300.0, 4472.0, 4171.0, 4343.0, 4515.0, 4214.0, 4386.0, 4085.0), ('Expenses', 'Insurance', 2945.0, 3069.0, 3193.0, 2976.0, 3100.0, 3224.0, 3007.0, 3131.0, 3255.0, 3038.0, 3162.0, 2945.0), ('Expenses', 'Depreciation', 4940.0, 5148.0, 5356.0, 4992.0, 5200.0, 5408.0, 5044.0, 5252.0, 5460.0, 5096.0, 5304.0, 4940.0), ('Other Expenses', 'Interest expense', 1092.5, 1138.5, 1184.5, 1104.0, 1150.0, 1196.0, 1115.5, 1161.5, 1207.5, 1127.0, 1173.0, 1092.5) ) AS t(section, account, "2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07", "2026-08") - 03Latest filing and ratios for each organizationcomputes running / windowed totals · filters to the relevant rows · sorts the output
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY ein ORDER BY fiscal_year DESC) AS rn, LEAD(total_revenue) OVER (PARTITION BY ein ORDER BY fiscal_year DESC) AS prior_revenue FROM input_1 WHERE total_revenue > 0 AND total_expenses > 0 ) SELECT role, organization, city, state, fiscal_year, form, ROUND(total_revenue, 0) AS total_revenue, ROUND(total_expenses, 0) AS total_expenses, ROUND(100.0 * (total_revenue - total_expenses) / total_revenue, 1) AS surplus_margin_pct, ROUND(100.0 * contributions_and_grants / total_revenue, 1) AS contributions_share_pct, ROUND(100.0 * program_service_revenue / total_revenue, 1) AS program_revenue_share_pct, ROUND(100.0 * (COALESCE(officer_compensation, 0) + other_salaries_and_wages + COALESCE(payroll_taxes, 0)) / total_expenses, 1) AS staff_cost_pct_of_expenses, ROUND(100.0 * officer_compensation / total_expenses, 1) AS officer_pay_pct_of_expenses, ROUND(net_assets / (total_expenses / 12.0), 1) AS months_of_net_assets, ROUND(100.0 * total_liabilities / NULLIF(total_assets, 0), 1) AS liabilities_pct_of_assets, ROUND(100.0 * (total_revenue - prior_revenue) / NULLIF(prior_revenue, 0), 1) AS revenue_growth_pct, filing_url, source FROM ranked WHERE rn = 1 ORDER BY role DESC, total_revenue DESC - 04Map each account to a benchmark line (check this step)aggregates rows into summary totals · buckets values by condition · filters to the relevant rows
WITH long AS ( UNPIVOT (SELECT * FROM input_1) ON COLUMNS(* EXCLUDE (section, account)) INTO NAME period VALUE amount ), recent AS ( SELECT DISTINCT period FROM long WHERE regexp_matches(period, '^[0-9]{4}-[0-9]{2}$') ORDER BY period DESC LIMIT 12 ), acct AS ( SELECT section, account, ROUND(SUM(amount), 2) AS last_12_months, (SELECT COUNT(*) FROM recent) AS months_covered FROM long WHERE period IN (SELECT period FROM recent) GROUP BY section, account ) SELECT section, account, last_12_months, months_covered, CASE WHEN section ILIKE '%income%' AND section NOT ILIKE 'other%' AND regexp_matches(lower(account), 'donat|contribut|grant|gift|bequest|pledge|event|appeal|sponsor') THEN 'Revenue - contributions & grants' WHEN section ILIKE '%income%' AND section NOT ILIKE 'other%' THEN 'Revenue - program & fees' WHEN section ILIKE 'other income%' THEN 'Revenue - investment & other' WHEN regexp_matches(lower(account), 'officer|executive director|\bceo\b|president') THEN 'Expense - officer pay' WHEN regexp_matches(lower(account), 'salar|wage|payroll|benefit|401|pension') THEN 'Expense - other staff cost' ELSE 'Expense - everything else' END AS benchmark_line, 'Mapped by section, then by words in the account name - check this step before relying on the comparison' AS mapping_note FROM acct ORDER BY CASE WHEN section ILIKE '%income%' THEN 0 ELSE 1 END, last_12_months DESC - 05Your measures from the booksbuckets values by condition · appends result sets (e.g. a TOTAL row)
WITH t AS ( SELECT SUM(CASE WHEN benchmark_line LIKE 'Revenue%' THEN last_12_months ELSE 0 END) AS revenue, SUM(CASE WHEN benchmark_line LIKE 'Expense%' THEN last_12_months ELSE 0 END) AS expenses, SUM(CASE WHEN benchmark_line = 'Revenue - contributions & grants' THEN last_12_months ELSE 0 END) AS contributions, SUM(CASE WHEN benchmark_line = 'Revenue - program & fees' THEN last_12_months ELSE 0 END) AS program_revenue, SUM(CASE WHEN benchmark_line IN ('Expense - officer pay', 'Expense - other staff cost') THEN last_12_months ELSE 0 END) AS staff_cost, SUM(CASE WHEN benchmark_line = 'Expense - officer pay' THEN last_12_months ELSE 0 END) AS officer_pay, MAX(months_covered) AS months_covered FROM input_1 ) SELECT 'surplus_margin_pct' AS measure_key, ROUND(100.0 * (revenue - expenses) / NULLIF(revenue, 0), 1) AS your_books, months_covered FROM t UNION ALL SELECT 'contributions_share_pct', ROUND(100.0 * contributions / NULLIF(revenue, 0), 1), months_covered FROM t UNION ALL SELECT 'program_revenue_share_pct', ROUND(100.0 * program_revenue / NULLIF(revenue, 0), 1), months_covered FROM t UNION ALL SELECT 'staff_cost_pct_of_expenses', ROUND(100.0 * staff_cost / NULLIF(expenses, 0), 1), months_covered FROM t UNION ALL SELECT 'officer_pay_pct_of_expenses', ROUND(100.0 * NULLIF(officer_pay, 0) / NULLIF(expenses, 0), 1), months_covered FROM t UNION ALL SELECT 'total_revenue', ROUND(revenue, 0), months_covered FROM t - 06Peer benchmark - where you standcombines data from multiple inputs · aggregates rows into summary totals · filters to the relevant rows
WITH peers AS (SELECT * FROM input_1 WHERE role = 'peer'), subj AS (SELECT * FROM input_1 WHERE role = 'subject' LIMIT 1), m(ord, measure_key, measure, unit) AS (VALUES (1, 'surplus_margin_pct', 'Surplus margin', '% of revenue'), (2, 'contributions_share_pct', 'Reliance on contributions & grants', '% of revenue'), (3, 'program_revenue_share_pct', 'Program & fee revenue', '% of revenue'), (4, 'staff_cost_pct_of_expenses', 'Staff cost', '% of expenses'), (5, 'officer_pay_pct_of_expenses', 'Officer pay', '% of expenses'), (6, 'months_of_net_assets', 'Net assets incl. buildings & endowment', 'months of expenses'), (7, 'liabilities_pct_of_assets', 'Liabilities', '% of assets'), (8, 'revenue_growth_pct', 'Revenue growth, latest filed year', '%') ), pl AS (UNPIVOT (SELECT organization, surplus_margin_pct, contributions_share_pct, program_revenue_share_pct, staff_cost_pct_of_expenses, officer_pay_pct_of_expenses, months_of_net_assets, liabilities_pct_of_assets, revenue_growth_pct FROM peers) ON COLUMNS(* EXCLUDE (organization)) INTO NAME measure_key VALUE v), q AS (SELECT measure_key, COUNT(*) AS peers, ROUND(QUANTILE_CONT(v, 0.25), 1) AS peer_25th, ROUND(QUANTILE_CONT(v, 0.5), 1) AS peer_median, ROUND(QUANTILE_CONT(v, 0.75), 1) AS peer_75th FROM pl GROUP BY measure_key), sl AS (UNPIVOT (SELECT organization, surplus_margin_pct, contributions_share_pct, program_revenue_share_pct, staff_cost_pct_of_expenses, officer_pay_pct_of_expenses, months_of_net_assets, liabilities_pct_of_assets, revenue_growth_pct FROM subj) ON COLUMNS(* EXCLUDE (organization)) INTO NAME measure_key VALUE v), j AS ( SELECT m.ord, m.measure, m.unit, b.your_books, sl.v AS your_last_filing, q.peer_25th, q.peer_median, q.peer_75th, q.peers, COALESCE(b.your_books, sl.v) AS yours FROM m LEFT JOIN q ON q.measure_key = m.measure_key LEFT JOIN input_2 b ON b.measure_key = m.measure_key LEFT JOIN sl ON sl.measure_key = m.measure_key ) SELECT ord, measure, unit, your_books, your_last_filing, peer_25th, peer_median, peer_75th, peers, CASE WHEN yours IS NULL THEN 'Peers only - not in the books; name your organization in the chat to add its own filings' WHEN yours < peer_25th THEN 'Below the peer middle half' WHEN yours > peer_75th THEN 'Above the peer middle half' ELSE 'Inside the peer middle half' END AS where_you_stand FROM j ORDER BY ord - 07The peers, each traceable to its filingfilters to the relevant rows · sorts the output
SELECT organization, city, fiscal_year AS filed_for_fiscal_year, form, total_revenue, surplus_margin_pct, contributions_share_pct, staff_cost_pct_of_expenses, months_of_net_assets, filing_url, source FROM input_1 WHERE role = 'peer' ORDER BY total_revenue DESC - 08BOARD SUMMARY - what to talk throughbuckets values by condition · appends result sets (e.g. a TOTAL row) · filters to the relevant rows
WITH b AS (SELECT * FROM input_1), p AS (SELECT COUNT(*) AS n, MIN(filed_for_fiscal_year) AS y0, MAX(filed_for_fiscal_year) AS y1, MIN(total_revenue) AS r0, MAX(total_revenue) AS r1, ANY_VALUE(source) AS src FROM input_2), scored AS (SELECT * FROM b WHERE where_you_stand NOT LIKE 'Peers only%'), rows_out AS ( SELECT 1 AS ord, 'PEER GROUP' AS area, (SELECT n || ' organizations, revenue $' || ROUND(r0 / 1e6, 1) || 'M to $' || ROUND(r1 / 1e6, 1) || 'M, filings for fiscal ' || CASE WHEN y0 = y1 THEN CAST(y1 AS VARCHAR) ELSE y0 || '-' || y1 END FROM p) AS finding, (SELECT 'None of this is private data - every peer links to its own public filing. Source: ' || src FROM p) AS next_step UNION ALL SELECT 10 + ord, upper(measure), CASE WHEN where_you_stand LIKE 'Peers only%' THEN 'Peers: ' || peer_median || ' ' || unit || ' (middle half ' || peer_25th || ' to ' || peer_75th || ')' ELSE 'You: ' || COALESCE(your_books, your_last_filing) || ' ' || unit || ' - peer median ' || peer_median || ' (middle half ' || peer_25th || ' to ' || peer_75th || ')' END, CASE WHEN where_you_stand = 'Inside the peer middle half' THEN 'None needed - inside the middle half of ' || peers || ' peers' WHEN where_you_stand LIKE 'Peers only%' THEN 'Not in the books - name your organization in the chat to add its own filings' ELSE where_you_stand || ' - worth a line in the board pack' END FROM b UNION ALL SELECT 99, 'VERDICT', (SELECT COUNT(*) FILTER (WHERE where_you_stand = 'Inside the peer middle half') || ' of ' || COUNT(*) || ' measures inside the peer middle half - ' || COUNT(*) FILTER (WHERE where_you_stand <> 'Inside the peer middle half') || ' to talk through with the board' FROM scored), 'Ratios only - peer filings are older than your books, so dollars are never compared across years' ) SELECT ord, area, finding, next_step FROM rows_out ORDER BY ord
Run this on your books
Free to try — no sign-up, no card. The workflow runs in your browser; your data never leaves your machine.
Make it your own →