Meeting extraction from three independent sources — Google Meet (via email), Fathom recordings, and Notion inbox — writing to a shared entity store. This domain is the exemplar for the entity pipeline pattern.
| Script | Description |
|---|---|
extract.ts |
Scans the email cache for meeting-related threads, detects source (Google Meet, Fathom), creates meeting packages with metadata and artifacts. For each new meeting it enqueues the Gmail meeting label (plus an archive if the message is in the inbox and not labelled watch, unless emailConfig.meetings.archive is false; see the pipeline block) on email-updates through the email pipeline's label-actions.ts, so nothing is enqueued while the message's account is report-only (emailConfig.reportOnly or its own accounts[].reportOnly; see email). Those actions are deferred, not dropped: see reportOnly catch-up. Gemini transcripts are not fetched here; the package gets gemini_link.txt and fetch-notes.ts exports the Doc |
fetch-notes.ts |
Walks meeting directories, fetches Gemini doc transcripts (exported as the meeting's source mailbox, meeting.json sources[0].account) and Fathom transcripts for meetings that need them |
ingest-notion.ts |
Polls a Notion inbox database, fetches meeting content via browser extraction, stages artifacts locally, archives the inbox page. One page per run: --page-id <id> (alias --meeting-id), else the page id in the override file, else the oldest inbox page older than --min-age <hours> (default 4); --dry-run reports the page and writes nothing |
flowchart TD
email["Email cache"] --> extract["extract.ts"]
extract --> packages["meeting packages\n(silo/meetings/id/)"]
packages --> fetch["fetch-notes.ts"]
fetch --> gemini["Gemini docs\n(gog export)"]
fetch --> fathom["Fathom transcripts\n(puppeteer / share page)"]
notion["Notion inbox"] --> ingest["ingest-notion.ts\n(browser extract)"]
ingest --> notionPkg["meeting packages\n(summary.txt, transcript.txt,\nmeeting.json)"]
Each meeting lives in a directory under {silo}/meetings/{meetingId}/:
meeting.json — canonical metadata (Zod-validated via meeting-schema.ts). participants holds email addresses (they derive the meeting id); people lists the participants who are in the people config as { id, name } (config.md) and is absent when none aretranscript.txt — extracted transcript (from Gemini, Fathom, or Notion)summary.txt — meeting summary (from Fathom or Notion)gemini_link.txt — Google Meet Gemini doc URLfathom-*.html — raw Fathom email HTMLgemini_link.txt; fetch-notes.ts then exports the Gemini doc transcript via gog as the meeting's source mailboxgog (email pipeline must be running for extract/fetch-notes)NOTION_API_KEY_PATH, and two refs in pipeline.refs (see the pipeline block): notion.inboxDataSourceId (the Inbox data source meetings-ingest-notion queries) and notion.archiveDatabaseId (the database each processed page is copied into)| Job | Schedule |
|---|---|
meetings-extract |
Every 13 min |
meetings-fetch-notes |
Every 19 min |
meetings-ingest-notion |
Every 23 min (disabled by default) |
| File | Purpose |
|---|---|
lib/meeting-schema.ts |
Canonical meeting.json Zod schema, writeMeetingMeta(), sort timestamp computation |
lib/detect.ts |
Meeting detection — subject matching, title normalization, participant extraction, Fathom URL detection, meeting ID generation |
lib/email-actions.ts |
Gmail actions for a new meeting (meeting label, archive if in the inbox and not watched; archive switchable via emailConfig.meetings.archive), enqueued via enqueueEmailUpdates (the per-account report-only gate) |
lib/pending-actions.ts |
Enqueues a new meeting's Gmail actions, or defers them while its account is report-only (handleNewMeetingEmailActions), and catches them up once it is not (see reportOnly catch-up) |
lib/package.ts |
Creates/updates meeting package directories and artifacts, manages runner-state index |
lib/meetings-dirs.ts |
Discovers all meetings directories across silos via getEntityDirs() |
lib/doc-fetch.ts |
Fetches Google Doc transcripts via gog CLI for meetings with gemini_link.txt |
lib/fathom-extract.ts |
Extracts transcripts from fathom-*.html files into transcript.txt |
lib/fathom-share-fetch.ts |
Fetches summary/transcript from public Fathom share pages using puppeteer-core |
lib/chrome-path.ts |
The Chrome/Chromium executable for puppeteer-core (PUPPETEER_EXECUTABLE_PATH, CHROME_PATH, well-known paths) |
lib/fathom-share-dom.ts |
Pure DOM extraction helpers for Fathom share page content |
lib/fathom-share-ingest.ts |
Pipeline for Fathom share meetings needing transcript fetching |
lib/notion-api.ts |
Notion API HTTP helpers with authentication |
lib/notion-browser-extract.ts |
Extracts meeting content from Notion public pages via gateway browser tool |
lib/notion-blocks.ts |
Notion block building: paragraph and heading blocks, text chunks, batched append |
lib/notion-inbox-processor.ts |
Processes a Notion inbox page end-to-end: fetch, extract, stage, archive |
While a meeting's account is report-only (emailConfig.reportOnly is true, or the account's own accounts[].reportOnly is), extract.ts still packages its new meetings but enqueues none of their Gmail actions. Instead it records each new meeting as pending (runner state namespace meetings, collection pendingEmailActions, keyed by source key).
Each run catches up to 20 pending meetings whose account is no longer report-only (CATCH_UP_PER_RUN in lib/pending-actions.ts), so a long report-only window drains gradually. Pending meetings of an account that is still report-only stay pending and do not count towards the 20:
watch, or already moved out of the inbox, is not archived. A message no longer in the cache gets the meeting label only.emailConfig.meetings.archive setting applies.enqueueEmailUpdates, the single per-account report-only gate. A meeting's pending record is removed once its actions are enqueued, so nothing is enqueued twice.Only meetings packaged while report-only after this behaviour was introduced are pending. Meetings packaged earlier have no record and are never caught up, so turning report-only off can't mass-archive old mail. A pending record that can't be read is moved, unchanged, to the pendingEmailActionsInvalid collection (same namespace) and named in a WARNING line in the run log. It is never retried: inspect it, then re-create or delete it by hand. To discard pending actions instead of applying them, delete the pendingEmailActions items in the meetings namespace before turning report-only off.