Granola
Granola covers meetings Fathom's bot doesn't catch — for example, calls where the Fathom bot got ejected or wasn't invited. It's a secondary, gap-filling source: Fathom stays the primary meeting-truth connector, and Granola notes that overlap an existing non-Granola meeting are deliberately skipped.
How it works
granola-sync polls the official Granola API (public-api.granola.ai/v1). As of when this was
last checked, Granola has no webhook product — polling is the only feed, which is why this
connector runs more frequently than a typical reconciliation poller.
Each run:
- Lists notes updated within the lookback window (
lookbackHours, 1–720, default 72 — the wide default window exists because Granola hides a note from the API until its summary and transcript generation finish, so a note can appear "late" relative to when the meeting actually happened). - For each note, checks whether it overlaps (±30 minutes) an existing non-Granola meeting source
(
coveredByExistingMeeting). If it does, the note is skipped — this is the gap-filler rule that keeps Fathom primary and prevents duplicate meeting records when both tools recorded the same call. - Ingests the remainder via the same shared
ingestMeetinghelper Fathom uses. - Emits
activity_events(soft-fail) and writes aconnector_checkpointsrow.
Outbound requests to the Granola API retry on 429/5xx responses up to 3 attempts, honoring the
retry-after header when present.
The function also runs within a wall-clock time budget (timeBudgetMs, 10s–140s, default 110s),
the same bounded-ingest pattern used by Fathom and HubSpot.
Configuration
| Setting | Value |
|---|---|
| Secret | GRANOLA_API_KEY — edge secret, falling back to a Vault row if unset |
| Onboarding | Client generates a workspace API key in Granola → Settings → Connectors → API keys |
| Status | Blocked on that client key until it's provided; otherwise built and ready |
Cron cadence
| Task | Schedule | Params |
|---|---|---|
sable.granola.poll | Twice hourly, :05 and :35 (5,35 * * * *) | {limit:25, lookbackHours:72} |
Table mapping
| Table | What's written |
|---|---|
sources | type='meeting', via ingestMeeting — same schema as Fathom-sourced meetings |
decisions, promises, tasks | Extracted per meeting, same pipeline as Fathom |
activity_events | Ingest activity trail (soft-fail emission) |
connector_checkpoints | Health, high-water-mark bookkeeping for the granola row |
sources is also read (not just written) — the coverage check queries existing meeting sources to
decide whether a Granola note overlaps one before ingesting it.
Request shapes
{ "dryRun": true }
{ "limit": 25, "lookbackHours": 72 }
Why polling instead of a webhook
Every other meeting connector in this section (Fathom) is built around a signed webhook as the primary path, with polling as reconciliation. Granola inverts that: polling is the only feed, because the Granola API doesn't offer a webhook product to subscribe to. That's also why this connector's schedule runs twice as often as Fathom's reconciliation sweep (every 30 minutes versus every 4 hours) — without a push notification, the polling cadence is the only lever available to keep latency reasonable.
This also shapes the lookback window. A webhook-fed connector only needs to reconcile a short trailing window, because most items arrive promptly on delivery. Granola's 72-hour default lookback exists because notes can appear late relative to when the meeting happened — Granola doesn't surface a note via its API until summary and transcript generation finish server-side, which can lag the actual call by hours. A shorter lookback would silently miss those late arrivals rather than catching them on a subsequent poll.
What can go wrong
- Granola is enabled but nothing ingests. Check that
GRANOLA_API_KEYis actually set — this connector ships "Built, needs client key," meaning the code path is live but blocked until the client generates and hands over a workspace API key from their Granola settings. - A meeting seems to be missing even though Granola recorded it. If Fathom also recorded that call, this is expected — the gap-filler rule intentionally skips a Granola note that overlaps an existing non-Granola meeting within 30 minutes, so Fathom's version stays the single source of truth for that meeting.
- A note appears days after the meeting happened. This is normal for Granola: the API doesn't expose a note until its summary and transcript finish generating on Granola's side. The 72-hour lookback window exists specifically to catch these late arrivals on a later poll rather than requiring an immediate re-run.
- Repeated 429s during a poll. The client already retries up to 3 times honoring
retry-after; if failures persist past that, checkconnector_checkpointsfor thegranolarow'serrorand confirm the workspace API key hasn't been rotated or revoked on Granola's side.
Where the code lives
supabase/functions/granola-sync/index.ts— poller and ingestsupabase/functions/fathom-sync/ingest.ts— sharedingestMeetinglogic used by both Granola and Fathomtrigger/connectors.ts—sable.granola.pollschedule