Recipes
What councils will decide on an issue
For an advocacy group or membership platform: the agenda items on one issue across a province, when each comes up, and who sits on each council.
After this recipe you can hand your own language model every agenda item on one issue across a province, with the meeting date and the members of each council, each with its link, and ask it to write a briefing for your members.
For: an advocacy group, an industry association or a membership platform.
Uses: GET /v1/agenda-items and GET /v1/officials. One records request for the agenda items, and one for each town's officials.
Steps
- List the agenda items tagged with your issue's topic, across the province, from a date on.
GET /v1/topicslists the topic ids. - For each town in the results, list its elected officials.
- Number each record with its link and hand the list to your language model.
An agenda is posted a few days before its meeting, so in production set FROM to today's date to see what is coming. The sandbox's meetings end in October 2026, so the example uses 2026-10-01.
The code
import os
import requests
BASE = "https://data.heytownhall.com"
HEADERS = {"Authorization": f"Bearer {os.environ['HTD_KEY']}"}
TOPIC = "short_term_rentals"
FROM = "2026-10-01"
QUESTION = "For our members who rent out homes: which councils will discuss short-term rentals, when, and who sits on each council?"
def get(path, params):
res = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=30)
body = res.json()
if not res.ok:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
return body["results"]
items = get("/v1/agenda-items", {"topic": TOPIC, "region": "BC", "from": FROM, "limit": 100})
records = [(f"{i['meeting_date']}, {i['jurisdiction_id']}, item {i['number']} {i['title']}. {i['plain_summary'] or i['recommendation']}", i["source_url"]) for i in items]
for town in sorted({i["jurisdiction_id"] for i in items}):
members = get("/v1/officials", {"jurisdiction": town, "limit": 100})
names = ", ".join(f"{m['title']} {m['name']}" for m in members)
records.append((f"Elected officials of {town}: {names}", members[0]["source_url"] if members else ""))
prompt = "\n".join([
QUESTION,
"Answer only from these records. Cite each claim by its number and keep its link. If the records do not answer the question, say so.",
"",
*(f"[{n}] {text} ({url})" for n, (text, url) in enumerate(records, 1)),
])
print(prompt) # Send this to your own language model.const BASE = 'https://data.heytownhall.com';
const headers = { Authorization: `Bearer ${process.env.HTD_KEY}` };
const TOPIC = 'short_term_rentals';
const FROM = '2026-10-01';
const QUESTION = 'For our members who rent out homes: which councils will discuss short-term rentals, when, and who sits on each council?';
async function get(path, params) {
const url = new URL(path, BASE);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers });
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.results;
}
const items = await get('/v1/agenda-items', { topic: TOPIC, region: 'BC', from: FROM, limit: 100 });
const records = items.map((i) => [`${i.meeting_date}, ${i.jurisdiction_id}, item ${i.number} ${i.title}. ${i.plain_summary ?? i.recommendation}`, i.source_url]);
for (const town of [...new Set(items.map((i) => i.jurisdiction_id))].sort()) {
const members = await get('/v1/officials', { jurisdiction: town, limit: 100 });
const names = members.map((m) => `${m.title} ${m.name}`).join(', ');
records.push([`Elected officials of ${town}: ${names}`, members[0]?.source_url ?? '']);
}
const prompt = [
QUESTION,
'Answer only from these records. Cite each claim by its number and keep its link. If the records do not answer the question, say so.',
'',
...records.map(([text, url], n) => `[${n + 1}] ${text} (${url})`),
].join('\n');
console.log(prompt); // Send this to your own language model.What the sandbox prints
For our members who rent out homes: which councils will discuss short-term rentals, when, and who sits on each council?
Answer only from these records. Cite each claim by its number and keep its link. If the records do not answer the question, say so.
[1] 2026-10-10, tolland-township, item 4. Short-term rental licensing bylaw amendments. Staff ask council to consider: short-term rental licensing bylaw amendments. (https://tolland-township.primegov.example/meetings/900023)
[2] 2026-10-07, marlow-county, item 5. Short-term rental licensing bylaw amendments. Staff ask the board to consider: short-term rental licensing bylaw amendments. (https://marlow-county.escribe.example/meetings/900015)
[3] Elected officials of marlow-county: Chair Helena Brightwater, Director Alice Penhallow, Director Owen Castellan, Director Priya Vantongeren, Director Tomas Ferrell (https://marlowcounty.example/council)
[4] Elected officials of tolland-township: Councillor Bea Lindqvist, Councillor Hal Moreton, Councillor Nora Quillfeather, Councillor Sam Okonkwo-Reyes, Mayor Gil Ashdown (https://tolland.example/council)Going further
- How each member voted on the issue before:
GET /v1/motionswith the sametopic, thenGET /v1/motions/{id}for each member's recorded vote. - The committees each member sits on:
GET /v1/council-appointments. - Run it every week with
FROMset to that day, and send the briefing to your members.
Applications and hearings near a project
For a builder or developer: the development applications on a project's street, their stage, and the public hearings and notices that mention it.
Follow one topic across towns
For a newsroom or research team: every new agenda item on one topic, in every town, from the change feed, ready for a brief.