This repository holds data tracking every item the Oakland Unified School District (OUSD) Board of Education has voted on through its General Consent Report since August 2019, and a pipeline to fetch, store and enrich that data via LLMs. Included in the dataset is the official text from Legistar, a short plain-language summary, dollar amounts, the outcome and votes, a vendor profile.
The data backs the Consent Report Tracker on the Oakland vs. the World blog, and this repo is made public for anyone who wants the data itself, or wants to see exactly how it's made.
The blog and this tracker are independent projects. It isn't an official OUSD platform and isn't affiliated with the district. All engineering, compute and hosting costs are donated by Aleph.
As of October 12, 2026, it covers 163 Board meetings, from August 14, 2019 to October 14, 2026. That's 12,149 consent items authorizing $2.03 billion in spending, plus $543.7 million in grants and other money coming in, across 2,202 vendors and partners. Not counted in those totals: $257.4 million in grant applications, $153.3 million in budget allocations such as Measure N and G1 plans, and $3.76 billion in payroll and vendor checks the Board ratified after they were paid.At most of its meetings, the OUSD Board approves tens of millions of dollars in one vote. The General Consent Report bundles anywhere from a handful to a few hundred contracts, grants and amendments, and Board members and the public usually get only a few days to read it, either as a long PDF or through Legistar, the district's agenda system. While the data is public it isn't easy to read, search, or follow from one meeting to the next.
I'm the parent of two OUSD students and have been building websites for over twenty years, and I've struggled to understand pretty much every consent report I've read. This project uses AI to work through each report as soon as it's posted. Every item gets a short summary, the money is added up the same way every time, and there's a running record of what was approved and what changed along the way. I don't think the district makes this hard on purpose; it doesn't have the time or staff to present it any other way. The hope is that the Board, district staff, reporters and families can work from the same set of facts about how money gets spent.
Everything here is built from OUSD's own public records, and the official text is always included. The summaries are there to help you read and understand it, but are AI-generated, so keep that in mind.
Some of what's attached to each item comes straight from Legistar: the title, the official action text, the file and agenda numbers, the presenting office, the vendor number, the funding source, attachments, and the history of actions and votes.
Much of the data is computed by ordinary code with no AI involved, like the meeting totals, most of the flags, which items belong to the same vendor, and which amendments go with which contract.
Then, some of it is written by AI and then checked. That covers the headline and summary, the category and action type (and, for special education items, a sub-category), the vendor name, any schools mentioned, the contract dates, the dollar amounts, and four of the flags. The model is (currently) Claude, made by Anthropic, but this may change as open-weight models improve and become less expensive and resource-intensive. Each AI-written record says which model and which version of the instructions produced it.
flowchart TD
L[OUSD Legistar] -->|every 30 min| I[Find meetings,<br/>read the agenda]
I --> R[(Official record<br/>data/raw)]
R --> E[Summary and facts<br/>AI]
E --> C{Checks<br/>in code}
C -->|biggest items,<br/>anything unclear| V[Independent second reading<br/>AI]
V --> B
C --> B[Build<br/>in code]
H[Corrections<br/>by a person] --> B
VP[Vendor profiles<br/>AI research, AI review,<br/>checks in code] --> B
R --> B
B --> P[(Published JSON<br/>and CSV)]
P --> S[oakvs.world/<br/>consent-tracker]
Legistar's meeting listings are broken for OUSD for whatever reason, so this pipeline works out the schedule another way. District staff tend to file agenda items with a target date long before the agenda is published, and once five or more items are filed for a date's consent report, that date counts as an upcoming meeting. When the agenda goes up, the meeting is matched to its Legistar record by the file numbers on it.
Once an agenda is official, the pipeline in this repo pulls the consent section of the agenda (via the Legistar public API) and each item's full record. The section is found by its heading and agenda letter, because Legistar's own "consent" marker isn't reliable. What comes back is saved as a snapshot in data/raw/, and that snapshot is the official record the rest of the process works from. Open meetings are re-checked every 30 minutes, so agenda changes show up within the hour, and outcomes are refreshed daily until every item has been acted on.
For each item, the AI reads only the official text. It writes a headline (110 characters at most) and a two- or three-sentence summary aimed at about an 8th-grade reading level, and it pulls out the facts: who the vendor is, which schools are named, the contract dates, the category, and the dollar amounts, including any prior and new totals and whether the amount is a yearly limit. Its instructions are in pipeline/prompts/. They ask for neutral wording and spelled-out acronyms, and tell it not to guess at anything the text doesn't say. Individuals who contract in their own name are described by their role rather than named in headlines, as a courtesy.
Each item gets one of 13 categories, chosen by the AI from the official text alone against a written codebook, pipeline/prompts/enrich.v6.md, and listed with definitions on the site's about page. A category says what the money or action is for: a busing contract for special education students is Special education, not transportation, and software bought for a reading program goes with the program, not under Technology. It isn't based on which office presented the item or what kind of organization the vendor is. Special education items also get one of five sub-categories (nonpublic schools and agencies; transportation; services and contract staff; programs and support; legal, compliance and policy), because that one category is about a quarter of all counted spending and a single label hid what was in it. Code checks what it can (every special education item must have a sub-category and no other item may), and like everything else the AI writes, a category can be corrected by a person through data/overrides/.
October 2026 re-categorization. The codebook was revised (prompt v5) and every item since 2019 was re-summarized under it, so the whole dataset follows one set of rules. What changed: a "Budget, finance & payments" category for the district's own budget plans and payment ratifications, which had been split across three categories; "Legal, insurance & risk" became "Legal, compliance & risk"; the after-school rule now requires the text itself to place a program outside the school day, after items like daytime arts classes had landed there; and the special education sub-categories were added. 1,865 of 12,063 items (15%) were reclassified, most of them into the new budget category or between the after-school and classroom categories; another 217 changed only because the legal category was renamed. Earlier summaries were written by Claude Sonnet agents under prompts v2 and v3 (and 263 by the v1 prototype); every record now says enrich.v5.md. The before-and-after comparison is in the commit that made the change.
Before a summary is published, code checks that every dollar amount appears word for word in the official text, and that the passage quoted as evidence is copied exactly. Nothing gets calculated, rounded or estimated. If a check fails, the errors go back to the AI and it gets up to three tries. An item that still doesn't pass shows only its official text, marked "Summary pending," until it's sorted out.
The 20 largest items in each meeting get a second, independent reading of the money, as do items that failed a money check, items where the first reading thought the official text itself had a mistake, and headlines that might name a person. The second reader never sees the first reader's answer. If the two disagree in a way that would change the totals, a third reading settles it by majority, and the comparison is done in code. A disagreement that's still significant (more than $10,000 and more than 1% of the item) gets flagged for a human to look at.
Corrections live in data/overrides/ and always win over what the AI wrote. A reviewed correction marks its item human_reviewed, and if it changes something that was already published, the item gets a dated note. A correction also carries over to the same file number at other meetings, as long as the official text is identical.
A build step puts it all together: the official record first, then the AI readings, then any corrections. It works out totals, flags and each item's review status, and writes data/published/ and data/exports/. Run it twice on the same inputs and you get identical files, down to the byte, and the tests check for that.
Organizations (never individuals) can get a short profile with a description and public contact details. An AI researcher looks the vendor up in public sources like its own website and state, IRS and federal registries. Before a profile is published, the researcher has to be confident it found the right organization, with at least two separate pieces of evidence. Then a second AI reviewer has to agree it's the same organization and that the sources back up the description. Finally, code fetches every page cited and confirms each phone number, email, address, registry ID and legal name appears there exactly as written. Anything that doesn't check out is left off. These profiles don't come from OUSD and can go out of date.
One organization can show up as several vendors when Legistar spells its name differently or OUSD gives it two vendor numbers. Code combines the certain cases, like vendor numbers that differ only by leading zeros. An AI judge (Claude Opus 5.5, prompt vendor-merge.v2) decides the rest. It's told that a wrong merge is much worse than a missed one, and code checks every decision. Each decision and its reason is logged in data/vendors/merge-decisions.json, and any of them can be undone by hand. Each vendor page also lists every Legistar record that carries its OUSD vendor number, in any year.
Every change becomes a commit in this repository with a readable message, like 2026-10-14: agenda posted — 87 items, and the website rebuilds afterward. The official text goes up first, usually within half an hour of an agenda appearing in Legistar, and the summaries follow once they pass their checks. Between the two, the git history is a complete record of every agenda revision, outcome and correction.
| Records | Model |
|---|---|
| Summaries, August 2019 – September 2026 (12,063 items) | Claude Sonnet, run as Claude Code agents (sonnet-agent, prompt enrich.v5) |
| Summaries, October 2026 (88 items) | Claude Opus 5.5 through the Claude API (claude-opus-5-5, prompt enrich.v6) |
| Second readings, August 2019 – October 2026 (3,477 items) | Claude Sonnet 5.5 through the Claude API (claude-sonnet-5-5, prompt verify.v2) |
| Vendor research (91 vendors, 56 published) | Claude Sonnet 5.5 through the Claude API (claude-sonnet-5-5) |
| Vendor research (1,911 vendors, 1,284 published) | Claude Sonnet, run as Claude Code agents (sonnet-agent) |
New items from October 2026 on are summarized by Claude Opus 5.5 through the Claude API (prompt enrich.v6, v5 plus one special-education tie-break), which also does the tie-breaking third readings. Their second readings come from Claude Sonnet 5.5 (prompt verify.v2). New vendors are researched by Claude Sonnet 5.5 with web search (prompt vendor-research.v3) and independently reviewed by Claude Opus 5.5 (prompt vendor-review.v2). They'll show up in the table above as they're written.
Switching models or instructions doesn't quietly rewrite older records. New instructions get a new version number and are compared against the existing summaries before they're used.
Each item with money shows one main number, which is what that vote approves.
For a new contract, that's the most the district has agreed to pay. Most contracts set a "not to exceed" ceiling, so the district may end up paying less, but it can't pay more without another vote.
For a change to an existing contract, it's only the new money. If a $100,000 contract goes up to $150,000, the vote counts as $50,000, since the original $100,000 was approved at an earlier meeting.
Totals count only what the Board actually adopted, and each action only once. Items that were withdrawn, failed, postponed or later repealed stay on the meeting's list but add nothing. When a meeting doesn't act on its consent report and the same items come back at the next one, they count at the meeting where they were adopted. If an item shows up twice on one agenda, it counts once. Each item's countsTowardTotals field says whether it's counted.
Payroll and accounts-payable warrants are a special case. Every few months the Board ratifies the paychecks and vendor checks the district has already issued, often hundreds of millions of dollars at a time. Those payments were made under contracts and salaries approved elsewhere, so counting them as spending would count the same money twice. They're listed with a "Ratifies payments already made" flag and totaled separately. Grant applications work the same way: applying for money isn't receiving it, and an award usually comes back to the Board to be accepted, so applications get a "Grant application" flag and their own total. The same goes for budget allocations, like Measure N and G1 plans, budgets and carryovers: they divide up money the district already has, which is then spent through contracts and stipends that come to the Board as their own items.
Grants and other money coming in are counted separately from spending. Cuts to existing contracts are noted on the item but aren't subtracted. Yearly limits ("up to $X per year") are totaled on their own, so they don't get mixed in with one-time amounts. Caps on what the district can earn by auctioning surplus property aren't spending, so they're left out.
Every number comes from the official text. If the text doesn't state an amount, the data doesn't either, and nothing is rounded in the data.
Flags point out how something was done. They don't say anything about anyone's motives, and most flagged items are routine and perfectly legal.
| Flag | What it means | Set by |
|---|---|---|
| Voted on separately | Taken out of the single consent vote: voted on separately, withdrawn, referred, postponed or decided at a later meeting | Legistar's record |
| After work began | The agreement started before this meeting, or it's a ratification with no start date | Code |
| No bid | Bought through another agency's contract, a state schedule, or a legal exception to bidding | AI, from the text |
| Raises existing contract | An expense where the text gives a previous total and a higher new one | Code |
| Large increase | Raises an existing contract by half or more | Code |
| Text discrepancy | A confirmed error in the official text, like numbers that don't add up or a title that doesn't match | Confirmed by code, a second AI reading, or a person |
| Delayed at earlier meeting | Postponed, continued or failed at an earlier meeting | Code, from Legistar's history |
| Yearly cap | Sets a limit per year instead of a total | Code |
| No total stated | A yearly cap over several years with no overall total in the text | Code |
| Multi-year | The term runs longer than one school year | AI, from the text |
| Time extension only | Moves the end date without adding money | AI, from the text |
| Emergency | The text describes emergency work or contracting | AI, from the text |
Every item also has a review status. auto_ok means it passed every check. needs_review means something is waiting on a person, and review.alerts says what. blocked means it failed a hard check, so only the official text is shown. human_reviewed means a person signed off, and pending means there's no summary yet. Right now 12,032 items are auto_ok, 117 are human_reviewed and 10 are needs_review.
The checks make the worst kind of mistake, a wrong or made-up dollar amount, very unlikely. A summary can still get the gist of an item wrong, though, and categories are sometimes a judgment call. When in doubt, go by the official text, which is always right next to the summary.
Coverage starts in August 2019 and only includes consent items. Items on the regular agenda aren't here, and neither are meetings without a consent report.
Legistar has some quirks the pipeline works around. Its "Pass" flag sometimes disagrees with the recorded motion, for instance, and items pulled from a consent report are sometimes decided at a later meeting. The known ones are listed in pipeline/README.md.
Vendors are grouped by their OUSD vendor number and name. A typo in a vendor number only gets reconciled when the names match too, so the same organization occasionally shows up twice.
Vendor profiles can be missing or stale. Some vendors have very little public presence, some share a name with other organizations, and some just couldn't be verified.
| If you want | Look at |
|---|---|
| A spreadsheet | data/exports/items.csv, with one row per item (including the official text). There's also meetings.csv, vendors.csv, and a CSV for each meeting in data/exports/meetings/. |
| What each column means | data/exports/datapackage.json, a Frictionless Data Package describing every column and its type |
| Everything, as JSON | data/published/, starting with index.json. Each meeting file has its items with history, votes, checks and attachments, and there are vendor files and upcoming.json for the next meeting. |
| Schemas | schema/, with a JSON Schema (draft 2020-12) for every file |
| A fixed version to cite | Tagged releases, archived on Zenodo with a DOI (see CITATION.cff) |
The files sit at stable paths, so you can read them straight from GitHub:
curl -s https://raw.githubusercontent.com/oakvs/ousd-consent-data/main/data/published/index.json | jq '.meetings[0]'import pandas as pd
items = pd.read_csv("https://raw.githubusercontent.com/oakvs/ousd-consent-data/main/data/exports/items.csv")There's a mirror on Codeberg too. The CSVs are UTF-8 with a header row. Google Sheets opens them as is; in Excel, use Data → From Text/CSV so accented letters and dashes come through properly.
Thank you for looking closely. A quick heads-up first: this project is maintained by one busy dad in his spare time. I read every report and I'll get to yours, but it may take a while, so please be patient.
The first thing to figure out is where the mistake lives. Every item on the tracker links to its official page on Legistar (the legistar_url column in the CSV). Open it and compare.
If the official record on Legistar has the same problem, it's an error in OUSD's own records, and the district is the only one who can fix it. This project copies those records as published. Contact the Office of the Board of Education, which manages Board agendas and Legistar, at [email protected] or (510) 879-1940 (weekdays, 8:30 to 4:30). If you're looking for documents that aren't on Legistar, OUSD takes Public Records Act requests at [email protected].
If Legistar is right and the tracker is wrong, that one's on me (and robots 🤖). That covers a summary, headline, dollar amount, category, flag or vendor profile that doesn't match the official record, or an item that's missing or out of place. Please open an issue using the "Something's wrong with an item" form. It asks for the meeting date, the item's file number, what's wrong and what the official text actually says, which is everything needed to check and fix it quickly. Confirmed fixes go into data/overrides/, and the item gets a dated correction note.
If you'd like to make the fix yourself, that's even better and greatly appreciated. CONTRIBUTING.md explains how.
This data is about public business: contracts, and the organizations that hold them. District staff email addresses that turn up in Legistar records are removed before anything is saved. People who contract in their own name aren't named in headlines and are never researched. Vendor profiles only include an organization's public business contact information.
data/
raw/ the official record: one Legistar snapshot per meeting
enrichments/ AI summaries and facts, with the model and prompt version for each
verifications/ second readings
overrides/ corrections, which always win over AI output
legistar/vendors/ every Legistar record for each vendor
vendor-research/ vendor profiles, with their checks and review
published/ what the website reads, rebuilt by the build
exports/ CSVs and datapackage.json, also rebuilt by the build
meetings.json the list of meetings
llm-state.json AI bookkeeping: monthly spending, and items that keep failing their checks
packages/consent-schema/ data schemas and helpers, shared with the website
pipeline/ the code: Legistar client, checks, build, AI steps, prompts, tests
schema/ JSON Schema generated from packages/consent-schema
You'll need Node 22 or newer.
npm ci
npm test # includes checking that the build reproduces the published data exactly
npm run consent:build # rebuild data/published and data/exports
npm run consent:run -- --dry-run # run one live update on a scratch copy without changing anythingThe AI steps need an ANTHROPIC_API_KEY. Without one, consent run still publishes the official text. pipeline/README.md covers how the pipeline works, and RUNBOOK.md covers running it day to day.
The code is under the MIT license. This project's own work, meaning the headlines, summaries, classifications, flags, second readings and vendor profiles, is under CC BY 4.0. If you use it, please credit "OUSD Consent Tracker, Oakland vs. the World (oakvs.world)" and link back here. The official Legistar text and records are public records of the Oakland Unified School District.
To cite a specific version, use the DOI of a tagged release. CITATION.cff has the details, and GitHub's "Cite this repository" button uses it.