# City Council & Public Meeting Transcripts (`catalyst-labs/city-council-meeting-transcripts`) Actor

Timestamped city council and county board transcripts tied to agenda items and official votes. Granicus, Swagit, Legistar and YouTube; type a city or paste URLs. $0.008 per audio minute; list meetings first for $0.002 each. Keyword alerts and new-meeting monitoring.

- **URL**: https://apify.com/catalyst-labs/city-council-meeting-transcripts.md
- **Developed by:** [Catalyst Labs](https://apify.com/catalyst-labs) (community)
- **Categories:** AI, Videos, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 transcribed meeting minutes

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 & Public Meeting Transcripts

### Why agents pick this

- **Transcripts tied to the agenda and the official votes** from Legistar or the minutes, not just a wall of speech-to-text.
- **Type a city name** ("King County, WA") or paste Granicus, Swagit, Legistar or YouTube URLs. The registry finds the meetings for you.
- **Check before you spend:** `mode: listOnly` costs $0.002 per meeting. Transcription is $0.008 per audio minute, and `maxAudioMinutes` caps it.
- **Monitoring built in:** `onlyNewMeetings` plus keyword alerts flag what changed since the last run.

Turn public meeting video into **timestamped transcripts linked to the official agenda and votes**. Works with city councils, county boards and regional bodies on Granicus, Swagit, Legistar and YouTube. Type a city name or paste meeting URLs.

### Quick start

1. Paste a meeting URL into **Meeting URLs** (the form is prefilled with a 17-minute Dallas meeting, about $0.14), or type a **City or county** such as `King County, WA`.
2. Optional: turn on **Agenda-item timestamps** and add **Keywords to find**.
3. Click **Start**. Each meeting becomes one dataset record; all transcripts are also saved as one Markdown file.

Not sure which meetings exist? Set **Mode** to *List meetings only* ($0.002 per meeting) first.

#### Input examples

```json
{ "sources": ["https://dallastx.new.swagit.com/videos/404279"], "alignAgenda": true, "keywords": ["budget", "bond"] }
```

```json
{ "city": "King County, WA", "bodyFilter": "Metropolitan King County Council", "daysBack": 14, "alignAgenda": true, "maxAudioMinutes": 300 }
```

```json
{ "city": "Dallas, TX", "mode": "listOnly", "maxMeetingsPerSource": 5, "onlyNewMeetings": true, "monitorId": "dallas-weekly" }
```

**Several places in one run** (results are tagged with `input`, and `OUTPUT.byInput` summarizes each place):

```json
{ "cities": ["Des Moines, IA", "Polk County, IA", "King County, WA"], "daysBack": 14, "maxMeetingsPerSource": 1, "alignAgenda": true, "generateDigest": true, "maxAudioMinutes": 400 }
```

**YouTube-only city** (Des Moines posts video only on YouTube; votes come from the city's minutes PDF). Wait up to 60 min inside the run if captions are not ready yet:

```json
{ "sources": ["https://www.youtube.com/watch?v=03nYvOP_Mtk"], "alignAgenda": true, "generateDigest": true, "captionWaitMinutes": 60 }
```

**County board with "Proceedings" minutes** (Polk County, IA: official MP3 plus CivicClerk minutes):

```json
{ "city": "Polk County, IA", "dateFrom": "2026-10-06", "dateTo": "2026-10-06", "alignAgenda": true, "generateDigest": true, "digestPoints": 5 }
```

**Newsroom or research use, names as printed in the record:**

```json
{ "city": "Polk County, IA", "daysBack": 30, "generateDigest": true, "includeRecordNames": true }
```

#### Daily monitor with automatic upgrade

```json
{ "cities": ["Des Moines, IA", "Polk County, IA"], "daysBack": 3, "onlyNewMeetings": true, "recheckPending": true,
  "monitorId": "my-newsroom", "alignAgenda": true, "generateDigest": true }
```

Schedule it daily in Apify Console (Schedules → Create → this Actor, saved input). New meetings get a preliminary digest the same week; each one is re-checked daily and upgraded to the verified record digest when the minutes appear.

#### Output example (shortened, from a real run)

```json
{
  "status": "succeeded", "platform": "swagit", "title": "Finance Committee", "date": "2026-10-19T00:00:00",
  "durationSeconds": 974, "transcriptSource": "speechToText", "wordCount": 1701, "agendaSource": "swagitIndex",
  "agendaItems": [{ "title": "Call to Order", "start": 2, "end": 61, "timestamp": "00:00:02",
    "url": "https://dallastx.new.swagit.com/play/404279/2",
    "alignment": { "method": "official_video_index", "status": "matched", "confidence": "high" },
    "outcomeStatus": "not_in_official_record" }],
  "segments": [{ "start": 0.52, "end": 13.44, "text": "Is everybody ready? All right, committee. It is 1118 a.m. and I am calling this meeting ... to order.",
    "url": "https://dallastx.new.swagit.com/play/404279/0", "confidence": 0.86, "lowConfidence": false }],
  "chargedEvents": { "audio-minute": 17, "agenda-alignment": 1 }
}
```

**Record-first digest** (real output, Des Moines City Council, Aug 3 2026, 116-min meeting):

```json
"digest": { "method": "record_first_rule_ranked", "verificationStatus": "verified", "deterministic": true, "contentHash": "4864710fd7a14d54",
  "pointCount": 5, "recordedVotes": 74, "names": { "mode": "private_names_omitted", "privateNamesOmitted": 2 },
  "points": [{ "rank": 3, "headline": "On the proposed 38th Amendment to the Urban Renewal Plan for the Metro Center Urban Renewal Area",
    "outcome": "Motion carried, 7-0.", "mover": "Mandelbaum", "seconder": "Gatto", "page": 11,
    "officialSourceUrl": "https://councildocs.dsm.city/minutes/as20260803.pdf#page=11",
    "watchUrl": "https://www.youtube.com/watch?v=03nYvOP_Mtk&t=3572s", "timestamp": "00:59:32", "timestampKind": "item",
    "rankSignals": ["public_hearing", "taxes_budget_fees"] }] },
"consentAgenda": { "itemCount": 50, "timestamp": "00:21:42", "itemsWithOwnTimestamp": 3, "itemsSharingGroupTimestamp": 47 }
```

Failed records carry `errorType`, `error`, `hint`, `retryable` and `chargedEvents: {}`. When captions or the official recording are not posted yet, the record has `status: "retry_later"`, a suggested `retryAfter` time and `retryAfterReason`, and is never charged.

### Output fields at a glance

| Field | What it is |
|---|---|
| `status` | `succeeded`, `listed`, `retry_later` or `failed` |
| `input` | The city or URL the record came from (use it to split multi-city runs) |
| `text` / `segments` / `markdown` / `srt` / `vtt` / `llmText` | Transcript in the formats you picked; segments have `start`, `end`, `url`, `confidence` |
| `agendaItems[]` | Official title, `start`, `timestamp`, `url`, `timestampKind` (`item` or `consent_group_shared`), `consentGroupId`, `alignment`, and the official `result`, `tally`, `mover`, `seconder`, `votes` |
| `consentAgenda` | The consent block: item count, shared timestamp, how many items have their own |
| `officialOutcomes[]` | Every motion found in the minutes, verbatim `quote` with `page` |
| `digest` | Opt-in digest (see below) |
| `transcriptSource`, `sourceChain` | Where the transcript came from and which sources were tried |
| `chargedEvents` | Exactly what this record was charged |

### What you get per meeting

- **Transcript.** Text, segments, Markdown and SRT. Every segment links to the exact second of the video and carries a speech-to-text confidence score with a low-confidence flag.
- **Agenda items.** Each item has its official title, a start time and a deep link. Each placement states its **method** (clerk's video index, title match, or LLM placement with verified evidence) and its status: matched, uncertain or not_located.
- **Official outcomes and votes.** These come only from Legistar or the published minutes: result, mover, seconder, tally, and how each member voted (yes/no/abstain/recused/absent). If the official record doesn't say, the output is `not_in_official_record`. **We never infer a vote from the video.**
- **Keyword hits** with deep links, plus `listOnly` mode, `onlyNewMeetings` monitors, and a `maxAudioMinutes` cap.
- **Stable IDs** (`schemaVersion` 1.0): jurisdiction (with OCD-ID and FIPS), body, meeting, agenda item, legislation, document (SHA-256), and official.
- **Optional digest** (`generateDigest`, $0.05 per meeting).
  - **Record-first (default when the minutes or Legistar have votes):** the top `digestPoints` decisions (default 5, exactly that many unless fewer qualify), ranked by fixed public rules: zoning and land use, ordinances, public hearings, taxes, budget and fees, contracts and property, dollar size, split votes, failed motions. Consent, routine items (minutes, claims, licenses, proclamations, receive-and-file) and set-a-date items are left out or ranked down. Every field comes from the official record with a page link, there is no AI-written text, and the same meeting always gives the same digest (`contentHash`). Status is always `verified`.
  - **No official votes yet** (minutes usually follow 1–3 weeks later): a **PRELIMINARY** digest, always labeled (`digest.preliminary: true`, `digest.label`). Choose it with `preliminaryDigest`:
    - `agenda` (default): rule-ranked official agenda titles with video links, no AI text and no outcomes. Deterministic. **Not charged.**
    - `ai`: AI draft-and-verify from the transcript, each claim checked against the transcript (`partially_verified` or `verified`). Charged as a meeting digest.
    - `off`: no digest.
  - **Automatic upgrade when the minutes post.** Every preliminary meeting gets a `recheck` block and goes on a re-check list (key-value store `council-pending-rechecks`, per `monitorId`). Schedule a daily run with `recheckPending: true` and the same `monitorId` (it can be the same scheduled run as your `onlyNewMeetings` monitor). Each pending meeting is re-run with its **cached transcript**, so audio minutes are **never charged twice**. When the minutes are posted it gets the verified record digest (`recheckResolved`, `digest.upgradedFrom`); **at most one digest is charged per meeting**, so if the preliminary digest was already charged the upgrade is free. If the minutes are still missing, the record comes back `minutes_pending` and nothing is charged. Meetings are dropped after `pendingMaxDays` (default 45) as `recheck_expired`. You can also re-check the meetings of one earlier run with `recheckDatasetId`.
- **Private individuals' names.** By default digest headlines and text leave out names of private individuals (public commenters, applicants, company officers), for example "Members of the public made comments" or "(company officer)". Officials, staff, companies and places stay. Set `includeRecordNames: true` to keep names exactly as printed in the official record; `digest.names.namedPeople` then lists each person with a role (`speaker`, `applicant`, `applicant_representative`, `official`, `named_in_record`) and the source page. The raw transcript is never changed either way.
- **Several places per run** with `cities`, and a built-in wait for late captions with `captionWaitMinutes`. A meeting that still has no captions comes back `retry_later` (free) and is added to the same re-check list, so the scheduled `recheckPending` run picks it up after its `retryAfter` time.
- **Short headlines, full titles.** Each digest point has a short `headline` (at most 110 characters, cut only at a clause boundary, never with "…") and the full `officialTitle` exactly as printed.
- **Stable timestamps.** Agenda-item times snap to the start of the transcript line that contains them (`timestampBasis: segment_start`), whole seconds, and the alignment step is cached by transcript and agenda, so re-running the same meeting gives the same times.

### Tested cities

**Transcripts and votes checked by hand against the official record (October 2026):**

- **Des Moines, IA:** YouTube captions; votes from the PrimeGov minutes. Oct 19 meeting: 52 of 52 item votes match the minutes (mover, seconder, tally, page), including two 6-1 votes.
- **Polk County, IA:** official MP3 recordings; "Proceedings" minutes. Oct 6 meeting: 38 of 38 item outcomes match, including one 3-0 vote with a member passing.
- **King County, WA:** Legistar votes. 4 meetings (Sep–Oct 2026): 39 of 39 recorded actions read match the Legistar record (mover, result and each member's vote); 3 untitled consent-agenda entries are not listed.
- **Seattle, WA:** Seattle Channel captions; Legistar actions. 4 meetings (Oct 2026): 32 of 32 recorded actions match.
- **San Jose, CA:** Granicus video; votes from the Legistar minutes. 5 meetings (Oct 2026): 30 of 30 actions read match the minutes (mover, seconder, result, tally); 1 procedural "orders of the day" vote is not listed. Outcomes from draft minutes say so.

**Transcripts, agenda timestamps and keyword alerts work; votes are not read for these places yet:**

- **Dallas, TX**, **Houston, TX** and **Austin, TX** (Swagit video and the clerk's video index)

**Meeting lists work (List-only mode):** Ames, Cedar Rapids and Davenport, IA. Phoenix, AZ lists meetings from Legistar, but its meetings have no linked video, so transcription fails for those meetings, without charge.

**Not supported yet:** Chicago, IL.

Other places on the same platforms often work but are not verified. **YouTube-only cities need captions:** YouTube often blocks audio downloads from cloud servers, so if a city posts video only on YouTube and has no other official recording, the transcript depends on captions being posted. Until they are, the meeting comes back `retry_later` and is not charged. Want your city tested? Open an issue on the Actor page.

### How we verify

- Official data wins: votes and decisions come only from the Legistar API or verbatim minutes quotes (with page numbers).
- A minutes-vs-video check marks contradictions `disputed` and withholds them.
- Documents are fingerprinted. Revisions, such as minutes approved later, are written to a corrections log.
- Digest claims are checked one by one against the cited transcript span, plus a neutrality lint. Vote and approval wording and private commenters' names are removed. Quotes must be verbatim. Anything unverified is dropped, and posts are withheld with reasons.

### Measured quality (October 2026)

- **Votes:** see Tested cities for hand checks against the minutes. Earlier checks: King County 132/132 fields and individual votes match the Legistar API.
- **Agenda placement:** King County 13 of 18 items placed by the clerk's index. Dallas 4/4. Ames 9/9. San Jose 6 matched, 1 uncertain, 3 not located.
- **Speech-to-text:** Whisper large-v3, about 2–3% word error rate on clean English. On council audio (Des Moines, Oct 2026) the transcript matched 92.3% of the words in the city's human-edited captions. Mean segment confidence was about 0.82 on the meetings tested.

### Limits

- **Consent items share one timestamp, on purpose.** They are adopted by one motion without discussion, so they are marked `timestampKind: "consent_group_shared"` with a `consentGroupId`; items pulled for discussion keep their own timestamp.
- **Digests are fully verified only when official votes exist.** Before the minutes are out, the digest is PRELIMINARY (AI-drafted and possibly `partially_verified`, or agenda-only). Check `digest.preliminary` and `digest.verificationStatus` before republishing, and use `recheckPending` to upgrade.
- **Transcript cache.** To avoid charging twice, the transcript of a meeting with a preliminary digest is kept in your key-value store `council-transcript-cache` until it is re-checked. Delete that store if you don't want it kept.
- **Privacy guard limits.** Names are detected by pattern: two capitalized words that are not an official, place or organization, plus single-word references next to a speech verb or title ("Doe said", "Ms. Roe"). A shared surname is not enough to be treated as an official: "Jane Sample" is removed even when a council member named John Sample is on the council. Lists of private property owners (for example tax-redemption petitions) are replaced by a count. Speakers' organizations are kept ("A speaker from the Drake Neighborhood Association"); vendors in contract titles are kept. Unusual name formats can still slip through, and an official whose first name is badly misheard may be replaced with "a speaker" (privacy wins). The raw transcript is the public recording, word for word, and is not redacted.
- **YouTube-only cities may need captions to be ready.** See below.
- **YouTube from the cloud and the fallback chain.** YouTube often blocks audio downloads from cloud servers. For a YouTube meeting the Actor tries, in order: human-made captions; the government's own recording for that date (for example Polk County's official MP3, or CivicClerk uploads for Davenport); auto-generated captions; the YouTube audio. Turn on `youtubeResidentialFallback` to add one last try through a US residential proxy. That try works only some of the time. Each record's `sourceChain` lists the steps tried. If nothing works yet, the record comes back as `retry_later` (`errorType: CAPTIONS_NOT_READY`) with a suggested `retryAfter` time and is not charged. Set `captionWaitMinutes` to have the run wait and retry on its own, or retry later, because captions and official recordings are often posted hours or days after the meeting. Des Moines posts its video only on YouTube. Its agendas and minutes come from PrimeGov, which has no recordings, so a Des Moines meeting's transcript depends on YouTube captions.
- **Votes are read only from official records.** If a vote can't be matched to its item with certainty, the item is shown without an outcome rather than with a guess. Procedural motions such as adjournment are not attached to items.
- No speaker names (no diarization). Council word error rate is estimated against edited captions (about 92% word match), not a verbatim reference.
- **Agenda platforms.** The Actor reads meeting calendars from BoardDocs, CivicEngage Agenda Center, CivicWeb, PrimeGov and CivicClerk, plus official pages that list meetings with video links. A recording is used when the platform links one: YouTube, Swagit, Granicus, Vimeo, MP4 or MP3. When it links none, the meeting appears in List-only mode, and in transcribe mode it fails with `NO_VIDEO`, not charged. For registry places, the agenda and minutes links are attached to the same-day video meeting.
- **Seattle Channel** (seattlechannel.org, used by Seattle's Legistar): the city's own closed-caption file is used as the transcript (`transcriptSource: officialCaptions`), the channel's agenda seek index places items, and votes come from Legistar.
- **Minutes posted only on the Legistar meeting page** (e.g. San José "Action:" minutes): read from the page's minutes link. Outcomes from DRAFT minutes are labeled `digest.recordStatus: draft_minutes`.
- Not supported: Facebook video (login wall), and sites that refuse our requests (HTTP 403). These fail with a clear error and are not charged.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| Transcribed meeting minute | $0.008 | Per minute of delivered transcript, rounded up once per meeting |
| Meeting listed | $0.002 | Per meeting in List-only mode |
| Agenda alignment | $0.05 | Per meeting, only when at least one item is located |
| Meeting digest | $0.05 | Per meeting when a digest is delivered, including digests labeled `partially_verified` (recordings under 3 minutes are skipped, not charged). At most once per meeting. Preliminary digests for meetings without minutes are free by default (`preliminaryDigest: "agenda"`); you pay $0.05 once, when the verified record digest is delivered. With `preliminaryDigest: "ai"` the AI preliminary digest is charged and the later upgrade is free. |

No start fee. Failed meetings are never charged. Example: a 2-hour meeting with agenda timestamps costs 120 × $0.008 + $0.05 = $1.01. Use **Max audio minutes per run** and the run's maximum charge to cap spending; the Actor stops before it would exceed either.

# Actor input Schema

## `city` (type: `string`):

City, county or other public body, e.g. "King County, WA", "San Jose" or "Polk County, IA". Built-in registry first (with state, county, FIPS and OCD IDs), then Legistar lookup. Leave empty if you paste URLs in "Meeting URLs". For several places in one run use "Cities" below (this field still works on its own).

## `cities` (type: `array`):

List of cities, counties or bodies, e.g. \["Des Moines, IA", "Polk County, IA", "King County, WA"]. Each is resolved like "City or county"; results are tagged with their input and summarized per place in the OUTPUT record (byInput). Can be combined with "City or county" and "Meeting URLs".

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

Any mix of: Granicus list pages (https://CITY.granicus.com/ViewPublisher.php?view_id=N) or meeting pages (MediaPlayer.php?clip_id=N); Swagit list pages (https://CITY.new.swagit.com/views/N) or videos (/videos/N); Legistar calendars (https://CITY.legistar.com or legistar:CLIENT); YouTube channels, playlists or videos; direct .mp4/.m3u8 recordings; or a meeting web page that embeds one. Unsupported sites fail with a clear message and are never charged.

## `mode` (type: `string`):

listOnly returns meeting metadata (title, date, length, video and agenda links) without transcribing. Use it to discover meetings cheaply, then transcribe the ones you want.

## `daysBack` (type: `integer`):

Look for meetings from the last N days (ignored when dateFrom is set).

## `dateFrom` (type: `string`):

Earliest meeting date (YYYY-MM-DD). Overrides daysBack.

## `dateTo` (type: `string`):

Latest meeting date (YYYY-MM-DD). Default: today.

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

Newest N meetings taken from each list page, channel, Legistar calendar or city.

## `bodyFilter` (type: `string`):

Only meetings whose body or title contains this text, e.g. "City Council" or "Planning Commission".

## `titleFilter` (type: `string`):

Only meetings whose title contains this text (case-insensitive).

## `alignAgenda` (type: `boolean`):

Adds agenda items with start/end times and video links. Uses the platform's own index when it has one (Swagit, some Granicus), otherwise aligns the Legistar agenda or agenda document to the transcript. Charged only when at least one item is located; no agenda found = no charge.

## `generateDigest` (type: `boolean`):

Adds a neutral plain-language digest: headline, the top decisions (see Digest points, default 5) with 'watch the moment' links, upcoming items, Markdown and ~280-character social versions. Vote results come only from official records (minutes or Legistar). When the official votes are available you get the verified record digest ($0.05, charged once per meeting). Before the minutes are posted you get a free PRELIMINARY digest from the agenda (see 'Digest when minutes are not posted yet'). Names of private individuals are left out by default.

## `digestMode` (type: `string`):

Record-first digests are built only from the official minutes/Legistar (no AI text), ranked by fixed rules, page-cited and identical on every run. They are always "verified".

## `digestPoints` (type: `integer`):

Exactly this many top decisions (fewer only if the meeting has fewer eligible official votes).

## `includeRecordNames` (type: `boolean`):

Off (default): digest headlines and text omit names of private individuals (public commenters, applicants, company officers); officials, staff, companies and places stay. On: names are kept exactly as printed in the official record and digest.names.namedPeople lists each person with role (speaker / applicant / applicant_representative / official / named_in_record) and source page. The raw transcript is never changed either way.

## `preliminaryDigest` (type: `string`):

Used only when the meeting has no official vote record yet. Every such digest is labeled PRELIMINARY and the meeting is put on the re-check list, so a later run with recheckPending upgrades it to the verified record digest. At most one digest is ever charged per meeting.

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

Words or phrases to find in each transcript, e.g. \["rezoning", "data center", "Main Street"]. Each hit includes the timestamp, surrounding text, agenda item and a link to that moment. Free.

## `onlyNewMeetings` (type: `boolean`):

Skip meetings this tool already delivered in earlier runs with the same sources (or monitorId). Schedule the run daily or weekly to get just new meetings.

## `monitorId` (type: `string`):

Name for the onlyNewMeetings memory, so several schedules can track separately. Optional.

## `recheckPending` (type: `boolean`):

Re-run meetings that got a PRELIMINARY digest (minutes not posted) or a retry_later result (captions not posted) in earlier runs with the same monitorId. The transcript is reused, so audio minutes are never charged twice; a meeting whose minutes are now posted gets the verified record digest (free if its preliminary digest was already charged). Schedule this daily. Works with or without new sources.

## `recheckDatasetId` (type: `string`):

Optional: ID of an earlier run's dataset. Items with a recheck block are re-checked (same rules as recheckPending).

## `recheckIntervalHours` (type: `integer`):

A pending meeting is re-checked at most this often (its retryAfter).

## `pendingMaxDays` (type: `integer`):

Pending meetings whose minutes or recording never appear are dropped after this many days (reported as recheck_expired, not charged).

## `maxAudioMinutes` (type: `integer`):

Spending cap: the run stops transcribing once this many audio minutes are used. 240 min = $1.92.

## `maxMinutesPerMeeting` (type: `integer`):

Meetings longer than this fail uncharged, unless truncateLongMeetings is on.

## `truncateLongMeetings` (type: `boolean`):

Transcribe only the first maxMinutesPerMeeting minutes of longer meetings instead of skipping them.

## `outputFormats` (type: `array`):

Formats included in each record.

## `language` (type: `string`):

Spoken language code (en, es...). Empty = auto-detect.

## `vocabulary` (type: `array`):

Names and terms to spell correctly, e.g. council members' names or street names.

## `granicusHost` (type: `string`):

Only needed when a Legistar city's Granicus site isn't CLIENT.granicus.com, e.g. "cityname.granicus.com".

## `youtubeCaptionsFallback` (type: `boolean`):

For YouTube meetings, use the video's own captions (human-made first, auto-generated after the city's official recording). Marked transcriptSource youtubeCaptions or youtubeAutoCaptions; same per-minute price.

## `youtubeResidentialFallback` (type: `boolean`):

Off by default. When captions and the city's official recording are both unavailable, try downloading the YouTube audio through Apify's US residential proxy. Works only some of the time and is slower. Failed attempts are never charged.

## `maxConcurrency` (type: `integer`):

Meetings transcribed at the same time.

## `jurisdiction` (type: `string`):

Registry id or name (e.g. 'us-ia-des-moines' or 'Des Moines, IA') used to tag meetings from pasted video URLs so the official agenda and minutes can be found. It does not add that jurisdiction's own sources.

## `captionWaitMinutes` (type: `integer`):

If a YouTube meeting has no captions yet and no official recording is posted, wait up to this many minutes inside the run and retry (nothing is charged while waiting; you pay only Apify compute time). 0 = return a free "retry_later" result with a suggested retryAfter time immediately.

## `captionRetryIntervalMinutes` (type: `integer`):

How often to re-check during the caption wait window.

## Actor input object example

```json
{
  "cities": [],
  "sources": [
    "https://dallastx.new.swagit.com/videos/404279"
  ],
  "mode": "transcribe",
  "daysBack": 30,
  "maxMeetingsPerSource": 1,
  "alignAgenda": false,
  "generateDigest": false,
  "digestMode": "auto",
  "digestPoints": 5,
  "includeRecordNames": false,
  "preliminaryDigest": "agenda",
  "onlyNewMeetings": false,
  "recheckPending": false,
  "recheckIntervalHours": 24,
  "pendingMaxDays": 45,
  "maxAudioMinutes": 60,
  "maxMinutesPerMeeting": 480,
  "truncateLongMeetings": false,
  "outputFormats": [
    "text",
    "segments",
    "markdown"
  ],
  "youtubeCaptionsFallback": true,
  "youtubeResidentialFallback": false,
  "maxConcurrency": 3,
  "captionWaitMinutes": 0,
  "captionRetryIntervalMinutes": 15
}
```

# Actor output Schema

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

Records with text, segments, Markdown, SRT/VTT, agenda items, keyword hits and meeting metadata.

## `summary` (type: `string`):

Counts, error types, audio minutes charged, stoppedBy.

## `markdown` (type: `string`):

All transcripts in one Markdown file (when outputFormats includes markdown).

# 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 = {
    "cities": [],
    "sources": [
        "https://dallastx.new.swagit.com/videos/404279"
    ],
    "maxAudioMinutes": 60
};

// Run the Actor and wait for it to finish
const run = await client.actor("catalyst-labs/city-council-meeting-transcripts").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 = {
    "cities": [],
    "sources": ["https://dallastx.new.swagit.com/videos/404279"],
    "maxAudioMinutes": 60,
}

# Run the Actor and wait for it to finish
run = client.actor("catalyst-labs/city-council-meeting-transcripts").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 '{
  "cities": [],
  "sources": [
    "https://dallastx.new.swagit.com/videos/404279"
  ],
  "maxAudioMinutes": 60
}' |
apify call catalyst-labs/city-council-meeting-transcripts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,catalyst-labs/city-council-meeting-transcripts"
        }
    }
}
```

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/aDAl66nBjRaXL3H7h/builds/c1Q46softMkYpEef5/openapi.json
