# Granola for Twenty

Sync [Granola](https://granola.ai) meeting notes into your Twenty CRM. For every
meeting, this app creates a `Note` on each participant `Person` (so the meeting
is visible on Person, Company, and Opportunity pages) and a `CallRecording`
carrying Granola's AI summary and transcript, linked to the matching
`CalendarEvent` — searchable, in context, and ready for AI agents and workflows
to act on.

## What this app does

1. Granola records and summarizes your meeting.
2. On a schedule (every 15 minutes) this app polls the Granola REST API for
   notes updated since the last window, and on install it backfills the last 30
   days.
3. For each note it fetches the full detail (summary, transcript, attendees,
   calendar event) from `GET /v1/notes/{id}?include=transcript`.
4. It resolves attendee emails to existing `Person` records and upserts:
   - a `Note` (title + a link back to Granola + the AI summary markdown) with one
     `NoteTarget` per resolved participant, and
   - a `CallRecording` (one per Granola meeting) with the AI summary and the
     transcript, linked to the matching `CalendarEvent` when one is found.

All writes are idempotent: deterministic ids mean re-syncing a meeting updates
the same records instead of duplicating them.

## Calendar event matching

Granola notes carry a `calendar_event` object. The app matches it to a Twenty
`CalendarEvent` with two strategies (mirroring the platform's calendar sync):

1. **External event id** — the note's provider event id
   (`external_id` / `google_calendar_event_id`) is matched against
   `CalendarChannelEventAssociation.eventExternalId`.
2. **iCalUID** — the note's `ical_uid` is matched against `CalendarEvent.iCalUid`.

If no calendar event matches (the meeting was never on a synced calendar, or
calendar sync is not configured in Twenty), the `CallRecording` is still created,
just without a linked calendar event, and the `Note` still delivers Person-page
visibility.

## Transcript

Granola's transcript is a flat string segmented only by coarse audio-source
markers (`Microphone:` for the local mic, `Speaker:` for remote audio) — there
are **no timestamps and no real attendee names**. The app parses it into ordered
speaker turns at write time and renders a simple scrollable list on the calendar
event's **Transcript** tab. The AI **Summary** tab is the primary surface.

## Setup

| Step | Where |
| ---- | ----- |
| 1. Get a Granola **Business-plan** API key | https://granola.ai → API settings |
| 2. Install the **Granola** app in Twenty | Settings → Apps → Granola → Install |
| 3. Paste the key into the `GRANOLA_API_KEY` application variable | Twenty app settings |

On install the app backfills the last 30 days; after that the cron keeps it up to
date.

## Configuration

| Application variable | Purpose |
| -------------------- | ------- |
| `GRANOLA_API_KEY` (secret) | Bearer token for `https://public-api.granola.ai/v1`. Used to list notes and fetch each note's transcript, summary, attendees, and calendar event. |

## Notes and limitations

- **Polling, not webhooks.** Granola does not expose webhooks, so completeness
  relies on an incremental overlap window plus a once-daily 24h re-sweep. A note
  updated only during a longer outage is picked up by the next daily sweep or a
  manual re-install/backfill.
- **Plain transcript.** No diarization or timestamps (Granola limitation).
- **Person matching by email.** A participant only gets a `NoteTarget` once a
  matching `Person` exists in Twenty (by primary email). Unmatched participants
  are linked on a later run after Twenty's own contact sync creates the Person.
- **No custom fields in v1.** The Granola meeting id is stored in the standard
  `CallRecording.externalRecordingId`; the Granola web URL is carried in the Note
  body.
