# City Council Agenda Scraper (Legistar, Granicus, CivicPlus) (`adaptive_depths/council-agenda-monitor`) Actor

Scrape city council, county board and planning commission agendas, minutes and agenda items from Legistar, Granicus, CivicPlus AgendaCenter, PrimeGov and CivicClerk. Keyword alerts with PDF text snippets.

- **URL**: https://apify.com/adaptive_depths/council-agenda-monitor.md
- **Developed by:** [ryan smith](https://apify.com/adaptive_depths) (community)
- **Categories:** News, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 council agenda monitors

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## City Council Agenda & Minutes Scraper (Legistar, Granicus, CivicPlus)

**Scrape and monitor city council, county board, planning commission and school board agendas and minutes from Legistar, Granicus, CivicPlus AgendaCenter, PrimeGov and CivicClerk, with keyword alerts and PDF text snippets.**

Track what your local governments are about to vote on without opening 20 different websites. You get **one clean record per meeting** with agenda, minutes, packet and video links, the agenda items, and the **exact passages** where your keywords (`rezoning`, `data center`, `police budget`...) appear in the agenda or minutes PDFs. Schedule it daily with *Only new meetings since last run* and it becomes an alert feed.

**Try it now:** the input is prefilled (Seattle Legistar + Wichita CivicPlus), so just click **Start**. A first run takes under a minute.

### Supported platforms

| Platform | What to enter | Agenda/minutes links | Agenda item list | Notes |
|---|---|---|---|---|
| **Legistar** (Granicus) | Client name, e.g. `seattle`, `oakland`, or `https://seattle.legistar.com` | ✅ | ✅ (with file numbers) | Uses the official public Legistar Web API |
| **CivicPlus AgendaCenter** | Site root or `https://www.city.gov/AgendaCenter` | ✅ | — (use PDF text) | All categories (boards) on the site, including past years |
| **Granicus** (classic ViewPublisher) | `https://<city>.granicus.com/ViewPublisher.php?view_id=N` | ✅ (+ video) | — (use PDF text) | |
| **PrimeGov** | `https://<city>.primegov.com` | ✅ | ✅ | |
| **CivicClerk** | `https://<tenant>.portal.civicclerk.com` or `civicclerk:<tenant>` | ✅ (+ packet) | ✅ | |

You can mix platforms in a single run.

### Use cases

- **Journalists and newsrooms:** a morning digest of every agenda posted in your coverage area; keyword alerts for `settlement`, `no-bid`, `emergency contract` or `consent decree` that return the paragraph, not just the PDF; a searchable archive of minutes.
- **Lobbyists and government-affairs teams:** monitor dozens of jurisdictions for ordinances that affect clients (`short-term rental`, `franchise fee`, `cannabis`, `5G`); see items **before** the meeting so there is time to comment or brief a council member; push results to a CRM, Slack or Google Sheets through Apify integrations.
- **Realtors and real-estate agents:** catch `rezoning`, `annexation`, `variance` and `subdivision plat` items early and tell clients what is coming to a neighborhood.
- **Developers and site selectors:** follow planning commission and zoning board agendas for `site plan`, `PUD`, `impact fee`, `data center` or `incentive` items across every market you are evaluating.
- **Civic tech and researchers:** build open meeting archives, vote trackers and transparency dashboards from structured JSON instead of scraping five platforms yourself.
- Also used by advocacy groups and community organizers (`affordable housing`, `bike lane`, `public hearing`) and by consultants who sell to local government.

### Verified cities and counties (tested Oct 16, 2026)

Each of these returned meetings with agenda links in a quick test fetch. Paste the value from the **Input** column into `sources`.

| Jurisdiction | Platform | Input |
|---|---|---|
| Seattle, WA | Legistar | `seattle` |
| Oakland, CA | Legistar | `oakland` |
| Boston, MA | Legistar | `boston` |
| Denver, CO | Legistar | `denver` |
| Mesa, AZ | Legistar | `mesa` |
| Kansas City, MO | Legistar | `kansascity` |
| Milwaukee, WI | Legistar | `milwaukee` |
| Columbus, OH | Legistar | `columbus` |
| Pittsburgh, PA | Legistar | `pittsburgh` |
| Baltimore, MD | Legistar | `baltimore` |
| Richmond, VA | Legistar | `richmondva` |
| Nashville, TN (Metro) | Legistar | `nashville` |
| Louisville, KY (Metro) | Legistar | `louisville` |
| Jacksonville, FL | Legistar | `jaxcityc` |
| San Jose, CA | Legistar | `sanjose` |
| Fresno, CA | Legistar | `fresno` |
| Sacramento, CA | Legistar | `sacramento` |
| Alameda, CA | Legistar | `alameda` |
| Corpus Christi, TX | Legistar | `corpuschristi` |
| El Paso, TX | Legistar | `elpasotexas` |
| Ann Arbor, MI | Legistar | `a2gov` |
| King County, WA | Legistar | `kingcounty` |
| Pima County, AZ | Legistar | `pima` |
| Clark County, NV | Legistar | `clark` |
| DeKalb County, GA | Legistar | `dekalbcountyga` |
| Fulton County, GA | Legistar | `fulton` |
| Hillsborough County, FL | Legistar | `hillsboroughcounty` |
| Wichita, KS | CivicPlus | `https://www.wichita.gov/AgendaCenter` |
| Murfreesboro, TN | CivicPlus | `https://www.murfreesborotn.gov/AgendaCenter` |
| San Marcos, TX | CivicPlus | `https://www.sanmarcostx.gov/AgendaCenter` |
| Ankeny, IA | CivicPlus | `https://www.ankenyiowa.gov/AgendaCenter` |
| Los Angeles, CA | PrimeGov | `https://lacity.primegov.com` |
| San Jose, CA | Granicus | `https://sanjose.granicus.com/ViewPublisher.php?view_id=60` |
| Lakewood, CO | CivicClerk | `https://lakewoodco.portal.civicclerk.com` |

Not every city exposes the Legistar API (for example Chicago, Philadelphia, Houston and Austin don't, and NYC needs a token). Any other city on a supported platform should work; see *Finding your city's platform* below.

### Input

| Field | Description |
|---|---|
| `sources` | List of portal URLs or Legistar client names (see the table above) |
| `startDate` / `endDate` | Meeting date range. Defaults: 30 days back to 30 days ahead |
| `keywords` | Case-insensitive matches. Single words match whole words; phrases work too |
| `downloadPdfText` | Download agenda and minutes (PDF or HTML), extract the text and return snippets |
| `includeFullText` | Also include the full extracted text in each record |
| `onlyKeywordMatches` | Output only meetings that match at least one keyword |
| `onlyNewSinceLastRun` | Monitoring mode. Skips meetings already delivered. A meeting is sent again when its minutes get posted |
| `includeItems` | Fetch structured agenda items (Legistar, PrimeGov, CivicClerk) |
| `maxMeetingsPerSource`, `maxPdfPages`, `snippetChars` | Limits |
| `legistarTokens` | Optional `{ "client": "token" }` for the few Legistar clients that require one (e.g. NYC) |

### Sample input

```json
{
  "sources": ["seattle", "denver", "https://www.wichita.gov/AgendaCenter"],
  "startDate": "2026-10-01",
  "endDate": "2026-11-15",
  "keywords": ["zoning", "housing", "budget"],
  "downloadPdfText": true,
  "onlyKeywordMatches": true,
  "maxMeetingsPerSource": 50
}
```

### Sample output (one item per meeting)

```json
{
  "platform": "legistar",
  "jurisdiction": "seattle",
  "body": "Select Budget Committee",
  "title": "Select Budget Committee",
  "date": "2026-10-15",
  "time": "9:30 AM",
  "location": "Council Chamber, City Hall 600 4th Avenue Seattle, WA 98104",
  "agendaUrl": "https://legistar2.granicus.com/seattle/meetings/2026/10/6766_A_....pdf",
  "minutesUrl": null,
  "detailUrl": "https://seattle.legistar.com/MeetingDetail.aspx?LEGID=6766&...",
  "items": [{ "number": "1.", "title": "Policy Considerations: Seattle Police Department ...", "fileNumber": null }],
  "itemCount": 17,
  "keywordMatches": ["budget", "police"],
  "matchedSnippets": [{ "keyword": "police", "source": "agenda", "snippet": "…Policy Considerations Seattle Police Department…" }],
  "agendaTextLength": 3927
}
```

Other fields: `meetingId`, `packetUrl`, `videoUrl`, `sourceUrl`, `agendaStatus` and `minutesStatus` (Legistar), `textErrors` (for example a scanned PDF with no text layer), and `scrapedAt`. A `RUN_SUMMARY` record in the key-value store lists any sources that failed and why.

### Tips

- **Finding your city's platform:** open the city's "Agendas & Minutes" page. A link to `*.legistar.com`, `*.granicus.com`, `*.primegov.com`, `*.civicclerk.com` or `/AgendaCenter` tells you which one it uses.
- **Legistar client name:** it's the subdomain. For `https://oakland.legistar.com`, enter `oakland`.
- **Monitoring:** schedule a daily run with `onlyNewSinceLastRun` and `onlyKeywordMatches` turned on, then connect a Slack or email integration.

### Pricing

Pay per event, starting **October 29, 2026**:

| Event | Price | When it is charged |
|---|---|---|
| Meeting | **$4 per 1,000 meetings** ($0.004 each) | each meeting record saved to the dataset |
| Document text | **$6 per 1,000 document texts** ($0.006 each) | each agenda or minutes document whose text is downloaded and extracted (only when `downloadPdfText` is on) |

You only pay for results. With `downloadPdfText` off, a run costs just the meeting fee. Examples: a newsroom tracking 25 jurisdictions daily (about 260 meetings and 400 documents a month) pays about **$3.50 a month**. Pulling 1,000 meetings without text costs **$4**. Apify platform usage is billed separately by Apify under your plan.

### FAQ

**Which platforms are supported?** Legistar (Granicus), Granicus ViewPublisher, CivicPlus AgendaCenter, PrimeGov and CivicClerk. You can mix them in one run.

**How do I find my city's input?** Open the city's "Agendas & Minutes" page. A link to `*.legistar.com` means Legistar (enter the subdomain, e.g. `oakland`); `/AgendaCenter` means CivicPlus (enter the site URL); `*.granicus.com/ViewPublisher.php?view_id=N`, `*.primegov.com` or `*.civicclerk.com` mean the others.

**Can I get alerts only for new agendas?** Yes. Turn on `onlyNewSinceLastRun` (and `onlyKeywordMatches`), schedule the Actor daily and connect an email, Slack or webhook integration. A meeting is sent again when its minutes are posted.

**Does it search inside the PDFs?** Yes, with `downloadPdfText` on. It returns each match with surrounding context (`snippetChars`). Scanned PDFs without a text layer can't be searched (no OCR) and are marked in `textErrors`.

**Why did one of my sources fail?** Some Legistar clients don't enable the public API or need a token (add it under `legistarTokens`). The run continues with the other sources and lists the failure in the `RUN_SUMMARY` record of the key-value store.

**Do I need a proxy?** Usually not. If a CivicPlus site rate-limits you, enable Apify Proxy.

**Is this legal?** It collects only public meeting records that governments publish for the public. Respect each site's terms and don't schedule runs more often than you need.

**Can I export to Google Sheets or CSV?** Yes. Every dataset can be downloaded as CSV, Excel or JSON, or sent to Google Sheets with an Apify integration.

### Limitations

- Scanned (image-only) PDFs have no text layer. No OCR is done; the record carries `textErrors`.
- Documents larger than 40 MB (usually full agenda packets) are skipped for text extraction.
- CivicPlus and Granicus don't publish structured item lists, so keyword search there depends on `downloadPdfText`.
- Some cities' Legistar instances don't enable the public API (for example Chicago) or require a token (NYC). The run reports a clear error for that source and continues with the rest.
- Only public data is collected. Respect each site's terms and don't schedule runs more often than you need.

# Actor input Schema

## `sources` (type: `array`):

One per line. Legistar: a client name (e.g. `seattle`), `legistar:seattle`, or `https://seattle.legistar.com`. CivicPlus: the site root or `/AgendaCenter` URL. Granicus: a `ViewPublisher.php?view_id=N` URL. PrimeGov: `https://<city>.primegov.com`. CivicClerk: `https://<tenant>.portal.civicclerk.com` or `civicclerk:<tenant>`.

## `startDate` (type: `string`):

Meetings on/after this date (YYYY-MM-DD). Default: 30 days ago.

## `endDate` (type: `string`):

Meetings on/before this date. Default: 30 days ahead (catches upcoming agendas).

## `keywords` (type: `array`):

Case-insensitive whole-word matches against meeting titles, agenda items, and (if enabled) agenda/minutes text.

## `downloadPdfText` (type: `boolean`):

Fetch agenda and minutes documents (PDF or HTML), extract text, and return matched snippets. Slower; one or two downloads per meeting.

## `includeFullText` (type: `boolean`):

Also put the full extracted text in each dataset item (large).

## `onlyKeywordMatches` (type: `boolean`):

If enabled, only meetings whose agenda/minutes match at least one keyword are saved to the dataset.

## `includeItems` (type: `boolean`):

Structured items where the platform exposes them (Legistar, PrimeGov, CivicClerk).

## `onlyNewSinceLastRun` (type: `boolean`):

Remembers meetings already output in a named key-value store. A meeting is re-emitted once its minutes appear.

## `stateStoreName` (type: `string`):

Use a different name per monitoring task.

## `maxMeetingsPerSource` (type: `integer`):

Maximum number of meetings to return from each source.

## `maxPdfPages` (type: `integer`):

Maximum number of pages read from each agenda/minutes PDF when extracting text.

## `snippetChars` (type: `integer`):

Number of characters of context shown on each side of a keyword match.

## `proxyConfiguration` (type: `object`):

Usually not needed. Some CivicPlus sites rate-limit datacenter IPs.

## `legistarTokens` (type: `object`):

Map of client name to API token for the few Legistar clients that require one, e.g. {"nyc": "YOUR_TOKEN"}.

## Actor input object example

```json
{
  "sources": [
    "seattle",
    "https://www.wichita.gov/AgendaCenter"
  ],
  "keywords": [
    "budget",
    "zoning",
    "housing"
  ],
  "downloadPdfText": false,
  "includeFullText": false,
  "onlyKeywordMatches": false,
  "includeItems": true,
  "onlyNewSinceLastRun": false,
  "stateStoreName": "council-agenda-monitor-state",
  "maxMeetingsPerSource": 25,
  "maxPdfPages": 60,
  "snippetChars": 160,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "legistarTokens": {}
}
```

# Actor output Schema

## `meetings` (type: `string`):

All scraped meetings (default dataset), shown in the Meetings table view.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [
        "seattle",
        "https://www.wichita.gov/AgendaCenter"
    ],
    "keywords": [
        "budget",
        "zoning",
        "housing"
    ],
    "maxMeetingsPerSource": 25,
    "proxyConfiguration": {
        "useApifyProxy": false
    },
    "legistarTokens": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("adaptive_depths/council-agenda-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sources": [
        "seattle",
        "https://www.wichita.gov/AgendaCenter",
    ],
    "keywords": [
        "budget",
        "zoning",
        "housing",
    ],
    "maxMeetingsPerSource": 25,
    "proxyConfiguration": { "useApifyProxy": False },
    "legistarTokens": {},
}

# Run the Actor and wait for it to finish
run = client.actor("adaptive_depths/council-agenda-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    "seattle",
    "https://www.wichita.gov/AgendaCenter"
  ],
  "keywords": [
    "budget",
    "zoning",
    "housing"
  ],
  "maxMeetingsPerSource": 25,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "legistarTokens": {}
}' |
apify call adaptive_depths/council-agenda-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adaptive_depths/council-agenda-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/cemp34y6U9LIyUTgk/builds/uROCHR7lmlrHJcMXb/openapi.json
