Recipes
A meeting digest
Turn one council meeting into a short digest of what was on the agenda and what was decided, with links to the minutes.
After this recipe you can turn a town's latest council meeting into a plain-text digest: each agenda item in order, its summary, and the motions moved on it with their outcome and vote count.
Uses: GET /v1/meetings and GET /v1/meetings/{id}, two records requests.
How it works
- List the town's council meetings, newest first, with
kind=council. - Take the first one with
minutes_read_atset. A meeting whose minutes are not yet read has its agenda but no motions. - Read that meeting. One request answers with its agenda items, documents, recordings and motions.
- Print each agenda item, then the motions whose
item_positionis that item'sposition.
The code
import os
import requests
BASE = "https://data.heytownhall.com"
TOWN = "port-ellery"
HEADERS = {"Authorization": f"Bearer {os.environ['HTD_KEY']}"}
def get(path, params=None):
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
meetings = get("/v1/meetings", {"jurisdiction": TOWN, "kind": "council", "limit": 10})["results"]
latest = next(m for m in meetings if m["minutes_read_at"] is not None)
meeting = get(f"/v1/meetings/{latest['id']}")
print(f"{meeting['title']}, {meeting['meeting_date']} ({meeting['jurisdiction_id']})")
print(meeting["source_url"])
for item in meeting["agenda_items"]:
print()
print(f"{item['number']} {item['title']}")
if item["plain_summary"]:
print(f" {item['plain_summary']}")
for motion in meeting["motions"]:
if motion["item_position"] == item["position"]:
print(f" Motion {motion['number']}: {motion['outcome']}, {motion['votes_for']} to {motion['votes_against']}")
print(f" Moved by {motion['mover']}, seconded by {motion['seconder']}. {motion['source_url']}")const BASE = 'https://data.heytownhall.com';
const TOWN = 'port-ellery';
const headers = { Authorization: `Bearer ${process.env.HTD_KEY}` };
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;
}
const { results: meetings } = await get('/v1/meetings', { jurisdiction: TOWN, kind: 'council', limit: 10 });
const latest = meetings.find((m) => m.minutes_read_at !== null);
const meeting = await get(`/v1/meetings/${latest.id}`);
console.log(`${meeting.title}, ${meeting.meeting_date} (${meeting.jurisdiction_id})`);
console.log(meeting.source_url);
for (const item of meeting.agenda_items) {
console.log();
console.log(`${item.number} ${item.title}`);
if (item.plain_summary) console.log(` ${item.plain_summary}`);
for (const motion of meeting.motions.filter((m) => m.item_position === item.position)) {
console.log(` Motion ${motion.number}: ${motion.outcome}, ${motion.votes_for} to ${motion.votes_against}`);
console.log(` Moved by ${motion.mover}, seconded by ${motion.seconder}. ${motion.source_url}`);
}
}What the sandbox prints
Regular Council Meeting, 2026-08-07 (port-ellery)
https://port-ellery.civicweb.example/meetings/900004
1. Data centre servicing study terms of reference
Staff ask council to consider: data centre servicing study terms of reference.
Motion R807-1: carried, 5 to 0
Moved by Ravi Ostrander, seconded by June Halvorsen. https://port-ellery.civicweb.example/meetings/900004/minutes.pdf
2. 2027 operating budget: first draft
Staff ask council to consider: 2027 operating budget: first draft.
Motion R807-2: carried, 3 to 2
Moved by Ravi Ostrander, seconded by June Halvorsen. https://port-ellery.civicweb.example/meetings/900004/minutes.pdf
3. Active transportation network plan update
Staff ask council to consider: active transportation network plan update.
4. Rezoning application for 412 Harbour Road to permit a six-storey residential building
Staff ask council to consider: rezoning application for 412 harbour road to permit a six-storey residential building.
5. Short-term rental licensing bylaw amendments
Staff ask council to consider: short-term rental licensing bylaw amendments.Going further
- Each member's vote is on
GET /v1/motions/{id}, one more request a motion. - What was said is in the meeting's transcript,
GET /v1/meetings/{id}/transcript, billed on the transcripts meter. A recording'shas_transcriptsays whether one is held. - Send it on. The digest is plain text, ready for an email or a chat message. Keep the
source_urllinks, so a reader can check each line against the minutes.