Developers · Schema
The warehouse schema
The fixed endpoints answer the common questions. When yours isn’t one of them, you can write SQL against the warehouse directly — 33 tables spanning 211,000 funders and 20.1 million grants from public IRS filings, plus the UK Charity Commission register. This page is what’s in it.
Most of what follows is caveats: which share of grant dollars a cause total actually covers, why filtering orgs to the latest year silently drops 90% of the sector, why recipient_country='IN' is India while recipient_state='IN' is Indiana. Each one is a query that runs fine and returns the wrong number.
Running a query
POST one statement to /api/v1/sql. It must be a single SELECT or WITH … SELECT — multiple statements, any write, and the file-reading functions are rejected. You query the named views below, not files.
curl -X POST https://data.useplinth.com/api/v1/sql \
-H "X-API-Key: $PLINTH_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{"sql": "SELECT funder_ein, sum(amount) AS total, count(*) AS n
FROM grants WHERE tax_year = 2023
GROUP BY 1 ORDER BY total DESC LIMIT 10"}
JSONTwo ceilings, and they bound different things. Results are capped at 2,000 rows — the response carries truncated: true when you hit it, and the fix is to aggregate in SQL rather than page through. Separately, a query is canceled after 30 seconds: the row cap limits what comes back, this limits the scan behind it, and a GROUP BY across every grant ever filed reads all of them before it truncates anything. A canceled query returns 400 — narrow it with a tax_year or funder_ein filter.
SQL needs a paid key — there is no free SQL tier — and each query costs one call from your monthly allowance, the same as any other endpoint. The tables marked Pro below additionally need the Pro plan; naming one on a lesser plan returns 403 rather than a partial answer. See pricing, or reach the same warehouse conversationally through Ask the data or the Claude connector.
Core — grants, organizations, funders
The grant graph itself: every e-filed 990 grant, every organization behind one, and the funder classification that separates institutional money from donor-advised pass-through.
grants
one row per grant, ~20.1M rows (the headline corpus figure; keep it in step with lib/universe.ts → UNIVERSE_GRANTS, which every page reads — this file is JSON and cannot import it, so…
recipient_name
ONE canonical display name per recipient EIN, ~955k rows. ein TEXT(9), name TEXT, source TEXT ('own_filed' | 'bmf' | 'filed'), spellings INTEGER. JOIN THIS FOR A GRANTEE'S NAME. Do NOT build a name…
grant_purpose
per-grant PURPOSE TEXT for the universe grants (the `grants` view leaves purpose_raw NULL on the 'universe' tier to stay lean; this side table carries the text).
orgs
ONE ROW PER ORGANIZATION (~793k total): each org's MOST RECENT 990 filing.
funder_kind
ONE ROW PER FUNDER (funder_ein): is this backer a real institution or donor-advised / pass-through money? Columns: funder_ein, name, return_type, n_grantees, n_grants, total_out, daf_cnt…
org_bmf
IRS Business Master File: ein, name, city, subsection, classification, foundation, deductibility, status, ruling, ntee_cd.
pub78
ein, deductibility_codes.
auto_revocation
ein, revocation_date, reinstatement_date.
ofac_sdn
ent_num, name, program, sdn_type.
service_area
ein, fips, level, name, source, confidence (resolved grant-making footprint).
intl_entity
cc, recipient_raw, cluster_id, canonical_name, kind (non-EIN recipients, clustered).
cofunder_edge_intl
cluster_id, funder_ein, first_year, last_year, total_amount, grant_count (co-funding of non-EIN entities).
DuckDB views you can SELECT from (read-only):
grants — one row per grant, ~20.1M rows (the headline corpus figure; keep it in step with lib/universe.ts → UNIVERSE_GRANTS, which every page reads — this file is JSON and cannot import it, so pipeline/eval/schema_doc_test.py asserts the two agree).
funder_ein TEXT(9, zero-padded), recipient_ein TEXT(9, zero-padded, may be NULL),
recipient_raw TEXT (grantee name as filed), recipient_city, recipient_state TEXT(2),
recipient_zip, recipient_country TEXT, amount BIGINT (USD), tax_year INTEGER,
purpose_raw TEXT (NULL on the 'universe' tier — the grant_purpose side table carries it).
recip_id TEXT — THE RECIPIENT IDENTITY, RESOLVED ONCE. Use this to GROUP BY or count distinct
recipients, never coalesce(recipient_ein, recipient_raw): three tiers, 'ein-<EIN>' when the EIN
resolved, 'intl-<cluster>' for a resolved foreign entity, 'nm-<hash>' for an unmatched name.
Counting the string key splits one entity across its spellings (World Health Organization ranked
as 4 recipients, University of Oxford as 215) and is also ~3x slower to distinct-count.
is_individual, is_unidentified, is_daf, is_related_party BOOLEAN — the ?exclude= classes,
precomputed per row. Filter on these directly (WHERE NOT is_daf) rather than re-deriving the
classification with joins; measured ~11x faster on a cold query. is_unidentified is the WIDER
set (recipients naming nobody as well as people); is_individual is people only.
source_tier TEXT ('matched' = rich per-funder rows, mostly absent here;
'universe' = the bulk rows, which leave city/purpose NULL). NOTE: there is NO stored cause/subject
column on grants — a grant's cause is the GRANTEE's NTEE major group. Derive it by joining
recipient_ein → org_bmf.ntee_cd and mapping the first letter (A=Arts & Culture, B=Education,
C=Environment, D=Animals, E/F/G/H=Health, I=Crime & Legal, J=Employment, K=Food & Nutrition,
L=Housing & Shelter, M=Public Safety & Disaster, N=Recreation & Sports, O=Youth Development,
P=Human Services, Q=International, R=Civil Rights, S=Community Improvement, T=Philanthropy,
U=Science & Tech, V=Social Science, W=Public Benefit, X=Religion, Y=Membership Benefit,
Z/other=Unclassified). CEILING: only grants with a matched recipient_ein carry an NTEE, so any
by-cause dollar total covers ~81% of grant dollars (the matched share) — say so when you answer a
cause/theme question.
recipient_country — the recipient's country from the 990 foreign-address block, a FIPS-10-4 code
(India='IN', United Kingdom='UK', Switzerland='SZ', Kenya='KE' — NOT ISO-3166). This is THE way to
find cross-border giving: filter `WHERE recipient_country = 'IN'`, NEVER `recipient_raw ILIKE '%india%'`
(that also catches Indiana, American Indian, Indian River, Indian health boards). It is populated only
on the 'universe' tier and NULL/empty for domestic (US) grants — so `recipient_country='IN'` means
India-the-country. WARNING: `recipient_state='IN'` is Indiana, a US STATE — a completely different
thing from country 'IN'. Foreign grantees carry no EIN (recipient_ein NULL), so group by recipient_raw
(or recipient_country for country-level rollups).
recipient_name — ONE canonical display name per recipient EIN, ~955k rows.
ein TEXT(9), name TEXT, source TEXT ('own_filed' | 'bmf' | 'filed'), spellings INTEGER.
JOIN THIS FOR A GRANTEE'S NAME. Do NOT build a name with MIN/MAX/any_value(recipient_raw): that
is whichever string sorts first among the rows YOUR filter selected, so the name changes with the
query — EIN 041564655 came back as 'BRANDEIS UNIVERSITY' filtered to MA and 'HOME BASE' filtered
to MA+2023, against a register that calls it MASS GENERAL BRIGHAM INCORPORATED (183 filers'
spellings for that one EIN). `spellings` says how many were seen; `source` says which register
answered. For recipients with no EIN there is no row — fall back to arg_max(recipient_raw,
amount), the spelling attached to the most money, never MIN or MAX.
grant_purpose — per-grant PURPOSE TEXT for the universe grants (the `grants` view leaves purpose_raw
NULL on the 'universe' tier to stay lean; this side table carries the text). Columns: funder_ein,
recipient_ein (may be NULL), recipient_raw, recipient_country (FIPS-10-4, same as grants), tax_year,
amount BIGINT, purpose TEXT. Use this to filter grants by a fuzzy THEME that no NTEE cause bucket
captures — e.g. "who funds health in India" = `SELECT funder_ein, sum(amount) FROM grant_purpose WHERE
recipient_country='IN' AND purpose ILIKE '%health%' GROUP BY 1`. purpose is as-filed free text, so
ILIKE on it (it is NOT NULL here, unlike grants.purpose_raw). Join to funder names via orgs on funder_ein.
orgs — ONE ROW PER ORGANIZATION (~793k total): each org's MOST RECENT 990 filing. It is a SNAPSHOT,
NOT a year panel — tax_year is that org's latest-filing year and DIFFERS across orgs (most are
2023–2024 because filings lag 12–24 months; only ~70k have a 2025 latest filing). So for "all" or
"current" nonprofits, query orgs with NO tax_year filter (~793k); do NOT write
`tax_year = (SELECT MAX(tax_year) FROM orgs)` — that returns only the ~70k orgs whose latest filing
is the single newest year, badly undercounting. return_type splits the universe: 990 (~374k public
charities), 990EZ (~267k small orgs), 990PF (~137k private foundations), 990T (~16k). For real
year-over-year revenue trends use the grants table (a true multi-year panel), not orgs.
Columns: ein TEXT(9), name, state TEXT(2), zip5, return_type, tax_year, mission TEXT (may be NULL),
total_revenue, total_expenses, net_assets, grants_paid, surplus, rev_contributions, rev_program,
rev_investment, rev_other, months_liquid.
funder_kind — ONE ROW PER FUNDER (funder_ein): is this backer a real institution or donor-advised /
pass-through money? Columns: funder_ein, name, return_type, n_grantees, n_grants, total_out,
daf_cnt (donor-advised funds it reports maintaining on 990 Schedule D), daf_grants (grants FROM those
funds), is_daf BOOL, daf_confidence (0..1), daf_signal ('schedule-d' = explicit from the filing, the
authoritative signal; 'name' = named DAF sponsor / fiscal sponsor; 'mega-fanout' = 50k+ grantees like
a corporate match conduit), funder_kind ('daf_or_passthrough' | 'private_foundation' | 'public_charity').
Use to separate donor-advised money from institutional funders: to find who REALLY funds an org (not
DAFs), join grants.funder_ein → funder_kind and filter `NOT is_daf`. A DAF grant is a donor you can't
re-approach as an institution — material context, e.g. INTELEHEALTH INC's top funders are largely DAFs.
org_bmf — IRS Business Master File: ein, name, city, subsection, classification, foundation,
deductibility, status, ruling, ntee_cd.
pub78 — ein, deductibility_codes.
auto_revocation — ein, revocation_date, reinstatement_date.
ofac_sdn — ent_num, name, program, sdn_type.
service_area — ein, fips, level, name, source, confidence (resolved grant-making footprint).
Join grants to names via orgs/org_bmf on ein. EINs are 9-char zero-padded strings — compare as
strings (use lpad(x,9,'0') if a literal might be short).
Conventions: org names (orgs.name, org_bmf.name) are stored UPPERCASE and as-filed. To turn a funder
or recipient NAME into an EIN, call find_org(name) — never guess LIKE '%Name%' (LIKE is
case-sensitive, so mixed-case patterns return 0 rows). For "who did X fund" / top-recipient rankings,
GROUP BY coalesce(recipient_ein, recipient_raw) — NOT recipient_raw alone. recipient_ein is the
reliable key and one org files under many name spellings (e.g. the American Red Cross, EIN 530196605,
appears a dozen ways), so grouping by raw text fragments and understates large recipients; coalesce
collapses the ~67% of grants that carry an EIN and falls back to raw name only for the null-EIN tail.
CAVEAT: that null-EIN tail is unresolved — a big grantee can ALSO appear as a separate null-EIN
raw-name block (e.g. a 'AMERICAN RED CROSS' block with no EIN, apart from its EIN'd rows), so a
"top recipients" list can still understate one whose EIN is missing on some filings; flag this when it
matters. Get the display name via coalesce(ro.name, g.recipient_raw) off a LEFT JOIN orgs ro ON
ro.ein = g.recipient_ein. Use ILIKE rather than LIKE for any text match, and IN (...) / a join rather
than a scalar subquery (which errors when it returns more than one row).intl_entity — cc, recipient_raw, cluster_id, canonical_name, kind (non-EIN recipients, clustered).
Foreign NGOs, governments and individuals have no US EIN, so they cannot appear in the EIN-keyed graph. They are clustered by country instead: recipient names that share a normalised form (or are near-identical by Jaro-Winkler) within one country become a single entity with a stable cluster_id. kind is org | person | gov.
Join to grants on recipient_raw where recipient_ein IS NULL, matching cc to recipient_country. Deliberately UNDER-merged: distinct names for the same body (MSF vs Doctors Without Borders) stay separate without a curated alias, so treat a cluster as a lower bound on an organisation, not a canonical identity.cofunder_edge_intl — cluster_id, funder_ein, first_year, last_year, total_amount, grant_count (co-funding of non-EIN entities).
The cross-border half of the co-funder graph. cofunder_edge is keyed on recipient_ein and therefore covers only recipients with a US EIN; grants to foreign NGOs, governments and individuals sit outside it entirely. This is the same edge shape one key over: join to intl_entity on cluster_id for the name and country, and to orgs/org_bmf on funder_ein for the funder.
To find who else funds a foreign recipient: resolve the name to a cluster_id via intl_entity, then select funder_ein from here. Entities are deliberately under-merged (see intl_entity), so a funder list is a lower bound.Asset intelligence — foundation investment holdings
ProWhat private foundations hold, from 990-PF Part II schedules: asset-class allocation, named managers and funds, concentration, impact exposure.
org_asset_profile
ONE ROW PER PRIVATE FOUNDATION with itemized holdings (~78k 990-PFs): its investment portrait. ein, org_name, state, city, return_type, total_assets_fmv, classified_assets_fmv,…
foundation_holdings
ONE ROW PER (foundation × holding): ein, canonical_name, fmv, entity_type, asset_class [public_equity|fixed_income|private_equity|venture|hedge|real_estate|cash|pri|art|crypto|unknown], vehicle,…
holding_entity
ONE ROW PER distinct holding name: canonical_name, co_holder_count (foundations holding it; high = public security/ETF, 1-3 = private fund), fmv, entity_type, asset_class, vehicle, manager,…
ASSET INTELLIGENCE (Pro) — foundation investment holdings, from 990-PF Part II schedules: org_asset_profile — ONE ROW PER PRIVATE FOUNDATION with itemized holdings (~78k 990-PFs): its investment portrait. ein, org_name, state, city, return_type, total_assets_fmv, classified_assets_fmv, classified_coverage_pct (share of assets we classified — treat low coverage as low confidence), public_equity_fmv, fixed_income_fmv, alts_fmv, pe_fmv, vc_fmv, hedge_fmv, impact_fmv, top_holding_fmv, top_holding_pct_assets (concentration), n_items, self_managed_flag (TRUE = holds no alternatives). Use for: foundations holding venture/PE (LP prospecting), self-managed mid-size foundations (AUM prospecting), founder-stock concentration, impact-investing. FMV columns NULL = none of that class. foundation_holdings — ONE ROW PER (foundation × holding): ein, canonical_name, fmv, entity_type, asset_class [public_equity|fixed_income|private_equity|venture|hedge|real_estate|cash|pri|art|crypto|unknown], vehicle, manager (canonical fund family/manager), is_impact, resolution_status. The workhorse — roll up by manager (who manages foundation money), asset_class, or join foundations sharing a holding (co-investment). holding_entity — ONE ROW PER distinct holding name: canonical_name, co_holder_count (foundations holding it; high = public security/ETF, 1-3 = private fund), fmv, entity_type, asset_class, vehicle, manager, is_impact, resolution_status (prefer 'corroborated_public'/'corroborated_private' for claims; 'review' = low confidence). Caveats: 990-PF only (public charities don't itemize); we see directly-held + named funds, NOT index-fund constituents (exposure is a floor, not total); value-dated to the filing. Cite filings; never publish 'review' rows.
Governance & board interlocks
ProOfficers, directors and trustees from 990 Part VII / 990-PF Part VIII, plus the shared-trustee graph and the related-entity families projected from them.
board_link
the warm-intro substrate: ONE ROW PER org-pair (e1 < e2) sharing ≥1 individual trustee/officer. e1, e2 (the two EINs), shared_people (count), possible_shared_people (COUNT only — the connecting NAMES…
org_families
ein → the family cluster of related entities under shared governance/control: ein, family_id, family_size (entities in the cluster; 1 = standalone), combined_revenue (summed across the cluster),…
GOVERNANCE & BOARD INTERLOCKS (Pro) — officers, directors, trustees & key employees from 990 Part VII /
990-PF Part VIII, plus the shared-trustee graph that powers warm intros ("who can introduce me to X"):
board_link — the warm-intro substrate: ONE ROW PER org-pair (e1 < e2) sharing ≥1 individual trustee/officer.
e1, e2 (the two EINs), shared_people (count), possible_shared_people (COUNT only — the connecting NAMES are not published: a per-org
roster lookup is what a filing shows, but a cross-org list of names makes the PERSON the
subject of the query, which is the thing this API does not do), same_state
(BOOL), conf (0–1: confidence the shared name is really the SAME person — rarity- and geography-weighted,
so a common name spread across many boards scores low). This answers "who shares a board member with
foundation X": query BOTH directions (WHERE e1 = ? OR e2 = ?). Treat conf below ~0.3 as a likely
name collision, not a real tie — say so rather than presenting it as a connection.
org_families — ein → the family cluster of related entities under shared governance/control: ein, family_id,
family_size (entities in the cluster; 1 = standalone), combined_revenue (summed across the cluster),
family_name (the largest-revenue member's name). The table is TOTAL — every org self-maps as a singleton
(family_size = 1), so filter family_size > 1 for real multi-entity systems; get a system's members via
its shared family_id. Use to see whether a foundation is one node in a larger system.
Caveats: rosters are the LATEST filing per org (not a time panel); name matching is heuristic — a shared
common name is NOT proof of the same human (that's exactly what board_link.conf encodes); 990-PF role
flags are sparse. Full multi-hop "Get me to X" path-finding is the dedicated warm-intro service, not one
SQL query — board_link gives you the one-hop ties to reason from.Government funding
ProPublic money flowing TO nonprofits — the flip side of `grants`. Federal from USASpending.gov, plus nine state checkbooks.
gov_funding_federal
FEDERAL funding a nonprofit RECEIVES. Columns: ein TEXT(9), fiscal_year INTEGER (2017–2025), kind TEXT ('grant' | 'contract'), awarding_agency TEXT, program TEXT (CFDA/program title), amount DOUBLE…
gov_funding_state
same shape for STATE funding, but only 9 states are covered (CT, NJ, VT, MD, MA, OR, DE, OK, FL) — NOT national.
GOVERNMENT FUNDING (Pro) — public money flowing TO nonprofits, keyed by ein (the flip side of `grants`,
which is philanthropy OUT). Federal from USASpending.gov + 9 state checkbooks, pre-aggregated:
gov_funding_federal — FEDERAL funding a nonprofit RECEIVES. Columns: ein TEXT(9), fiscal_year INTEGER
(2017–2025), kind TEXT ('grant' | 'contract'), awarding_agency TEXT, program TEXT (CFDA/program title),
amount DOUBLE (obligations USD — committed, may be NEGATIVE for a deobligation/clawback), award_count
BIGINT. Join to org name/cause via orgs on ein. Use for "how much federal funding does org X get",
"which nonprofits depend most on federal money", agency/program breakdowns. CAVEAT: amount is
OBLIGATIONS not outlays, and the most recent fiscal year is partial (reporting lag) — a late-year drop
is a floor, not final. Exact pre/post-administration cuts by calendar date need the award-level file
(with action_date), which lives in the local warehouse, not this rollup.
gov_funding_state — same shape for STATE funding, but only 9 states are covered (CT, NJ, VT, MD, MA, OR,
DE, OK, FL) — NOT national. Columns: ein, state TEXT(2), fiscal_year, agency, program, category, amount
DOUBLE, award_count. Don't present state totals as a US-wide picture; scope answers to those 9 states.UK Charity Commission register
England & Wales charities from the Commission's monthly bulk extract — a different key (registered_charity_number, not EIN) and a different currency (GBP).
uk_charity
one row per charity-entry (397,713; 185,429 Registered, of which 171,642 main). registered_charity_number BIGINT (the key), linked_charity_number, charity_name, charity_registration_status…
uk_charity_finance
the annual-return HISTORY, one row per charity per financial year (1.2M): registered_charity_number, fin_period_start_date, fin_period_end_date, total_gross_income, total_gross_expenditure,…
uk_charity_return_b
the FULL financial return for larger charities (79k rows; smaller ones file only Part A, so coverage is partial BY DESIGN — say so).
uk_charity_return_a
Part A of the return, filed by ALL sizes (660k rows): grant_making_is_main_activity BOOLEAN (a direct grantmaker flag), income_from_government_grants, income_from_government_contracts,…
uk_charity_classification
registered_charity_number, classification_type ('What'|'Who'|'How'), classification_description. 'How' carries 'Makes Grants To Organisations'/'To Individuals' — the register's own self-declared…
uk_charity_area
geographic_area_type / _description (+ parent_*) — where a charity operates.
uk_charity_other_name
charity_name_type ('Working name'|'Previous name') + charity_name, for 124k charities.
uk_charity_governing_document
charitable_objects (the charity's formal purpose text, good for thematic search), governing_document_description, area_of_benefit.
uk_charity_policy
policy_name per charity (safeguarding, investment, conflicts of interest, …).
uk_charity_event
register events: event_type, date_of_event, reason, and the associated charity (assoc_registered_charity_number) — mergers, transfers, linkages.
uk_charity_published_report
the register's index of reports the Commission has PUBLISHED about a charity, one row per report, keyed on registered_charity_number.
uk_ingest_meta
date_of_extract (the Commission's OWN extract date), source, staged_at, table_name, row_count.
UK CHARITY COMMISSION REGISTER (free) — the first non-US jurisdiction in this warehouse, from the
Commission's monthly bulk extract (England & Wales ONLY: Scottish (OSCR) and Northern Irish charities
are NOT here, so never say "UK-wide" about these rows). Keyed on registered_charity_number, NOT ein:
these rows are NOT in `orgs` and do NOT join to grants on an EIN. To link a US grant to a UK charity
match on NAME — a fuzzy, best-effort join, so present matches as candidates, never resolved facts:
SELECT g.recipient_raw, c.registered_charity_number, c.charity_name
FROM grants g JOIN uk_charity c ON upper(g.recipient_raw) = upper(c.charity_name)
WHERE g.recipient_ein IS NULL;
Also try uk_charity_other_name, which holds the working/previous names a 990 grant line is likelier to
carry than the formal registered name. Money here is GBP, not USD — never add it to a US total or
compare the two without saying so.
TWO FILTERS YOU ALMOST ALWAYS WANT, because the extract is the FULL register:
• linked_charity_number = 0 — the main charity. Non-zero rows are subsidiary/linked entries that
share a registered_charity_number; summing without this double-counts.
• charity_registration_status = 'Registered' — 'Removed' charities (212k of the 398k rows) are
dissolved/deregistered. They're kept ON PURPOSE (a grant to a since-removed charity is exactly
what you want to detect), but they must never be counted as the live sector.
uk_charity — one row per charity-entry (397,713; 185,429 Registered, of which 171,642 main).
registered_charity_number BIGINT (the key), linked_charity_number, charity_name,
charity_registration_status ('Registered'|'Removed'), charity_type ('Charitable company'|'CIO'|
'Trust'|…), date_of_registration, date_of_removal, latest_income DOUBLE (GBP), latest_expenditure
DOUBLE, charity_contact_address1..5, charity_contact_postcode, charity_contact_web,
charity_activities (free-text description of what it does), charity_company_registration_number,
charity_insolvent, charity_in_administration, charity_is_cio, charity_gift_aid, charity_has_land.
NO email/phone — contact values are deliberately not staged.
uk_charity_finance — the annual-return HISTORY, one row per charity per financial year (1.2M):
registered_charity_number, fin_period_start_date, fin_period_end_date, total_gross_income,
total_gross_expenditure, date_accounts_received, accounts_qualified. Use for UK trends over time.
uk_charity_return_b — the FULL financial return for larger charities (79k rows; smaller ones file
only Part A, so coverage is partial BY DESIGN — say so). Income split
(income_donations_and_legacies, _charitable_activities, _investments, _other_trading_activities,
_legacies, _endowments), expenditure split (expenditure_charitable_expenditure, _raising_funds,
_governance, _support_costs, _investment_management, _depreciation, _total), balance sheet
(assets_total_fixed, assets_long_term_investment, assets_cash, assets_total_liabilities,
funds_endowment/_restricted/_unrestricted/_total, reserves), count_employees.
*** expenditure_grants_institution is UK GRANTMAKING — grants made to other organisations. It is
the closest analogue of a US funder's grants_paid: 7,265 charities report it, £70.4bn total. Use it
to SIZE and RANK UK grantmakers — but NOT to count them (see below). ***
uk_charity_return_a — Part A of the return, filed by ALL sizes (660k rows):
grant_making_is_main_activity BOOLEAN (a direct grantmaker flag), income_from_government_grants,
income_from_government_contracts, count_volunteers, employees_salary_over_60k + count_salary_band_*
(senior-pay distribution), trustee-benefit and fundraising-practice declarations.
uk_charity_classification — registered_charity_number, classification_type ('What'|'Who'|'How'),
classification_description. 'How' carries 'Makes Grants To Organisations'/'To Individuals' — the
register's own self-declared grantmaker flag.
*** THE TWO GRANTMAKER SIGNALS MEASURE DIFFERENT POPULATIONS AND BARELY OVERLAP: 74,442 charities
self-declare 'Makes Grants To Organisations', but only 7,265 report expenditure_grants_institution
— 69,549 declare without ever reporting an amount, and just 4,893 do both. That is not a data error:
Part B is filed only by LARGER charities, so the money column is structurally blind to the small
end (18,494 of the declared grantmakers have income under £10k). So: COUNT grantmakers from the
classification, SIZE them from expenditure_grants_institution, and never present a Part-B-derived
count as "the number of UK grantmakers" — it undercuts the real figure roughly tenfold. ***
uk_charity_area — geographic_area_type / _description (+ parent_*) — where a charity operates.
uk_charity_other_name — charity_name_type ('Working name'|'Previous name') + charity_name, for 124k
charities. The alias side of the US-grant name join above.
uk_charity_governing_document — charitable_objects (the charity's formal purpose text, good for
thematic search), governing_document_description, area_of_benefit.
uk_charity_policy — policy_name per charity (safeguarding, investment, conflicts of interest, …).
uk_charity_event — register events: event_type, date_of_event, reason, and the associated charity
(assoc_registered_charity_number) — mergers, transfers, linkages.
uk_charity_published_report — the register's index of reports the Commission has PUBLISHED about a
charity, one row per report, keyed on registered_charity_number. Inquiry reports, regulatory
case reports and similar. Presence of a row is a regulatory-attention signal, not a finding —
a published report can be routine, and reading one as wrongdoing would be defamatory as well
as wrong. Columns are staged straight from the Commission's extract without a fixed schema on
our side, so run DESCRIBE uk_charity_published_report for the current column list.
uk_ingest_meta — date_of_extract (the Commission's OWN extract date), source, staged_at, table_name,
row_count. Quote date_of_extract when giving UK figures: the register lags reality, and this slice
lags the register.UK trustees & the charity-pair graph
ProThe named UK trustee roster and the charity pairs projected from it — the UK analogue of `people` and `board_link`.
uk_charity_trustee
the named UK trustee roster, 921,733 rows: registered_charity_number, trustee_id, trustee_name, trustee_is_chair, individual_or_organisation ('P' person, 914,748 / 'O' organisation, 6,985),…
uk_board_edge
uk_charity_trustee projected to CHARITY PAIRS that share a trustee (85,631 rows, built by uk_board_graph.py). e1, e2 (registered_charity_number, always e1<e2), shared (how many trustees in common),…
uk_charity_trustee (Pro) — the named UK trustee roster, 921,733 rows: registered_charity_number,
trustee_id, trustee_name, trustee_is_chair, individual_or_organisation ('P' person, 914,748 / 'O'
organisation, 6,985), trustee_date_of_appointment. The UK analogue of `people`; trustee_id is stable
across charities, so shared trustees are found by trustee_id, not by name-matching. Filter to 'P'
for human interlocks — the top name-shares are corporate trustee services (Ludlow Trust and similar
sit on 100+ boards each) and are not a social connection.
uk_board_edge (Pro) — uk_charity_trustee projected to CHARITY PAIRS that share a trustee (85,631
rows, built by uk_board_graph.py). e1, e2 (registered_charity_number, always e1<e2), shared (how
many trustees in common), weight (Newman 1/(k-1) — a trustee on many boards contributes less to
each pair), overlap (shared / the SMALLER board), tie_kind, structural BOOLEAN.
*** ALWAYS filter `tie_kind = 'interlock' AND NOT structural` for anything user-facing. The other
rows are real but mean something different: tie_kind='family' (overlap>=0.8, 5,550 rows) is ONE
committee running two legal entities — a Methodist circuit and its trust, a diocesan fund and its
board of finance — not two organisations that happen to share someone; 'affiliated' (>=0.5) is the
grey zone. `structural` marks denominational ties (both charities declared 'Religious Activities',
or every shared trustee is clergy), which are administrative structure rather than a relationship
anyone can act on. Unfiltered, the strongest edges in this table are church admin and the answer
will be wrong in a way that sounds authoritative. structural has ~86% recall, so say "mostly
excluded", not "excluded". This is England & Wales only, and it does NOT join to US orgs. ***Figures come from public IRS filings, which the IRS releases on a 12–24 month lag, so every row is dated to its fiscal year rather than to today; organizations that don’t e-file may be absent entirely. UK rows come from the Charity Commission’s monthly bulk extract, cover England and Wales only, and are denominated in GBP. Funding relationships are reported as association, never as causation.