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
Make it your own →
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

  1. 01Peer filings - Michigan humane societies (live, public Form 990s)
    SELECT * FROM read_csv_auto('NONPROFIT_PeerFilings.csv', header=true, types={'ein': 'VARCHAR'})
  2. 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")
  3. 03Latest filing and ratios for each organization
    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
  4. 04Map each account to a benchmark line (check this step)
    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
  5. 05Your measures from the books
    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
  6. 06Peer benchmark - where you stand
    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
  7. 07The peers, each traceable to its filing
    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
  8. 08BOARD SUMMARY - what to talk through
    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 →