//Data
Ask in plain English: the AI voter query
Facts checked against official sources · September 9, 2026
The query screen is a text box. You type a question the way you would ask a data analyst, press Enter, and a few seconds later you have a count, a preview grouped by household, and a button that turns the result into a list you can walk, text, email, or mail. No SQL, no filter builder, no analyst on retainer.
The part that matters for a campaign handling licensed voter data is what happens between the text box and the database. The model never touches a voter row. It writes a small, strictly typed description of the filters you asked for, and three independent layers of server code check that description before any query runs. This page walks through that pipeline, shows what refinement looks like, gives example questions, and is explicit about what the engine will refuse to do.
What it is
Query lives under Voters next to Records and Lists. The entry screen shows a large prompt box, a rotating strip of example questions, your recent sessions with their result counts, and the lists you have already saved. A query becomes a session; each follow-up question inside it is a numbered step, and every step keeps its own count, preview, and cost so you can click back to any earlier step.
Results open on a page that shows the matching count, a preview of up to 500 rows grouped by household by default, a breadcrumb of steps, a Refine box, and a floating actions button that offers Export CSV, Create email list, Create SMS list, Create block-walking list, and Create mailer list. Save as list writes a static list with the full refinement chain attached, which is what lets a list be refreshed against new data later.
Behind the screen is the same voter database that structured search uses, so a question asked here and the same filters set on the Records page return identical rows. The difference is that here you describe the audience and the engine builds the filter.
How it works: from sentence to saved list
Every question runs through the same pipeline. The stages below are the ones the code actually implements, in order.
- Pre-flight caps. Before any model call, the server checks a per-user hourly query limit and per-campaign daily and monthly query budgets. A blocked query says which cap it hit and when it resets; nothing is billed.
- Context assembly. The server gathers what the model may reference: the campaign's state, the tags visible to you (platform, campaign, and your personal tags, each with its id), and the annotation keys this campaign actually has answers for, such as receptiveness or top issue.
- Neighborhood resolution. Neighborhood and subdivision names in your prompt are resolved server-side before the call by fuzzy-matching them against the names present in the data. An exact hit becomes a filter, a set of matching names becomes an in-list, and an unknown name is reported back rather than guessed.
- The model call. Claude receives a cached system prompt describing the allowed columns, attribute keys, election-history shapes, and rules, plus your question, and must answer through a single structured tool rather than free text.
- The output. Either a JSON query DSL (column filters, election filters, attribute filters, tag filters, annotation filters, sort, limit, household grouping, a suggested list name, and any warnings) or a typed error: not a voter query, not supported, or ambiguous.
- Layer one, the parser. The tool output is inspected for error shapes at the top level and nested inside the DSL before anything is treated as a query. An output that is neither clearly a query nor clearly an error is thrown out rather than guessed at.
- Layer two, the validator. The DSL is parsed with a strict schema that rejects unknown keys and unknown operators, checks every attribute key against an allowlist, injects the state filter if the model forgot it, and injects the campaign's hard scope (its district or county). A filter the model emits on the scope column is dropped and replaced, so no prompt can widen the universe.
- Layer three, the orchestrator. Sanity checks catch the failure modes a schema cannot: a prompt full of filter words that produced an empty DSL is rejected instead of returning random voters; a prompt that names a party but produced no party filter is rejected; a tag id you cannot see or an annotation key the campaign does not have is rejected.
- Compile. The validated DSL compiles to one parameterized SQL statement against three restricted views for voters, attributes, and election history. Every value that came from the model is a bind parameter; nothing from the prompt is ever interpolated into SQL text.
- Execute. The statement runs inside a transaction that sets the campaign id and your user id as session variables, switches to a dedicated read-only database role, and applies a statement timeout. The views enforce campaign scoping and the county-roll gate at the source, so off-roll voters never appear.
- Log. The prompt, the DSL, the compiled SQL, the row count, token usage, cost, and latency are stored on the step. Nothing about a query is invisible after the fact.
Refining, saving, and what happens with zero results
The Refine box under the results takes a follow-up in plain English. The rule the engine enforces is that each step is the full cumulative query, not a delta: prior filters are preserved and new ones narrow. If you change a filter type ("now only independents" after a Republican filter) the old filter is replaced rather than stacked into a contradiction. If you say "start over", "remove the age filter", or "broaden", the engine honors it; if the model silently drops a filter without that intent, the server unions the dropped filter back in and logs it.
A zero-result step gets two escape hatches. Try broader search sends a refinement that removes the narrowest filter and says which one it dropped. Need help getting the right data? opens a short form (who, where, party or lean, vote history, size, deadline, what it is for) that files a data-analysis support ticket for a person to build the list by hand.
Save as list re-executes the final step and materializes the matching voters as a static list, with the whole refinement chain stored on it. On the list page, Refresh re-runs only the final step against current data and saves the result as a new list with the date in its name, so the original stays a frozen snapshot. Refine on a list opens a new session scoped to that list's members; the scope is injected at compile time and cannot be dropped by a follow-up.
Example questions a local candidate types
These are the kinds of prompts the engine is built for. Mix geography, party, vote history, tags, survey answers, and contact presence in one sentence.
- Republicans 50 and older who voted in 3 of the last 5 primaries
- Independents under 35 in the Oak Hollow subdivision with a phone number
- Voters tagged 'definite supporter' who do not have a yard sign tag yet
- Households with three or more registered voters that voted in every general since 2018
- People 65 and older who voted by mail in the 2024 general but have not voted since
- Democratic primary voters in commissioner precinct 3 who rarely vote in November
- Everyone we marked receptiveness 4 or 5 on a walk who has an email address
- Never-voters under 30 in ZIP codes 78701 and 78702
How a local campaign uses it
City council, district seat, 19,000 registered. The candidate types "voters in my district who voted in at least 3 of the last 4 general elections" and gets 6,240. She refines with "only ones with a phone number" and lands on 2,910, then clicks Create SMS list. The list opens in the outreach composer with the suggested name already filled in. Total time: under two minutes, and the chain is on the list if anyone asks how it was built.
School board, at-large, 41,000 registered. The manager asks for "households with 3 or more registered voters who vote in every general" and gets 1,150 households. He saves it as a block-walking list, and the walk generator cuts it into a dozen routes of about 95 doors each. Two weeks later, after the walks, he asks the same session "only the ones we have not visited yet" and the engine uses the visit history to shrink the list.
County party, 14 precinct chairs. The data director asks for "Republican primary voters who did not vote in this year's primary" and gets 18,400. She creates a mailer list, which saves a static snapshot for the print vendor. After early voting starts she opens the list, hits Refresh, and gets a new list of 11,900 with the people who have since voted removed, without retyping anything.
Why it matters
The biggest gap between a well-funded campaign and a first-time local candidate is not data, it is the person who knows how to turn a file into a target universe. The query engine collapses that. A candidate who has read how to read a voter file can express the segments the file supports in the words she already uses, and the engine tells her the count before she commits volunteers or money to it. How many votes it takes to win becomes a number she can check against a list.
The design also protects the campaign from the classic failure of AI over a database: a confident answer that is quietly wrong. Because the model never writes SQL and three layers reject anything that does not translate cleanly, a refused or ambiguous prompt surfaces as an error message you can rephrase, not as ten thousand random rows saved to a list.
What it cannot do
The engine is deliberately narrow. Knowing the edges keeps you from wasting a query.
- It is not a chatbot. It only translates voter-targeting questions; anything else (billing, walk scheduling, general trivia) is refused as not a voter query.
- It never reads voter rows, so it cannot summarize a person, write about an individual, or answer "what do voters think" from the data. Survey and annotation answers are filters, not text the model sees.
- Every filter is ANDed. "Precincts 1 and 2" cannot be expressed as one query and the engine tells you to run one per precinct; "voted in any of these years" is supported through a single election filter with a list of years.
- It cannot widen scope. The campaign's district or county is injected as a base filter after the model runs, and any filter on that column is overwritten.
- Neighborhood names must exist in the data. An unknown name returns a not-supported error with the closest matches rather than a silent substitute like a ZIP or a street match.
- Fields depend on your file. A question about an attribute your upload does not carry returns nothing, and vendor-licensed attributes are only joined for sources the campaign holds a grant for.
- Caps and timeouts: the default result limit is 100,000 rows with a hard ceiling of 500,000, the on-screen preview is 500 rows, CSV export from a session is 10,000 rows, and a statement that exceeds the timeout shows its stored count with a too-heavy-to-preview notice instead of failing the page.
- Rate limits apply per user and per campaign, and the daily and monthly budgets are set per campaign rather than being unlimited.
- Session history is personal. You can delete your own sessions; managers cannot delete yours, and a saved list survives the deletion of the session that made it.
Works with
Every result is a list, so the engine feeds the rest of the platform directly. It reads the voter database, respects the same tags and filters as search, lists, and tags, and hands lists to block walking, SMS outreach, email outreach, and direct mail. Survey answers captured through scripts and surveys become filters here the next day.
Frequently asked questions
Does the AI see my voters' names, addresses, or phone numbers?
No. The model receives your question plus a description of the allowed columns and filter shapes, and it returns a structured filter description. The query itself runs later under a read-only database role scoped to your campaign, and the rows come back to your browser, not to the model. Anthropic is a subprocessor for the translation step only.
Which model powers it?
Claude, called through the Anthropic API with a cached system prompt and forced structured output. The model name is recorded on every step along with token counts and cost, so usage is auditable per campaign.
What happens when it cannot translate my question?
You get an error message, not a bad list. The model can return not-a-voter-query, not-supported, or ambiguous with an explanation, and the server independently rejects empty or contradictory filter sets. Rephrase with concrete criteria (party, age, ZIP, precinct, vote history, a known neighborhood) or use the zero-results help form to have a person build it.
Can I trust the count?
The count is the exact number of rows the compiled query returned against your campaign's current universe, with off-roll voters already excluded. The same filters set on the Records page return the same rows. What the count cannot tell you is whether your file's fields are complete, which is why the field coverage panel on import matters.
Who on my team can use it, and who can export?
The candidate and campaign manager always can. Staff can when an admin grants them the Voters tab. Exporting a session to CSV is limited to the candidate and campaign manager and capped at 10,000 rows; staff can still save the result as a list and send it to walks or outreach.
Is there a limit on how many queries I can run?
Yes. There is a per-user hourly rate limit and per-campaign daily and monthly budgets, checked before every model call. When a cap is hit, the message tells you which one and when it resets; nothing is charged for a blocked query.
Ask the question. Get the list.
Keep reading
More from Features