Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Granola to HubSpot Connector

Sync finalized Granola meeting notes to HubSpot as notes attached to the relevant contacts (meeting attendees).

Local Setup

  1. Install dependencies:

    pip install -r requirements.txt
  2. Copy .env.example to .env and add your HubSpot access token:

    cp .env.example .env
  3. Get a HubSpot private app access token:

    • Go to HubSpot Settings > Integrations > Private Apps
    • Create a new private app with these scopes:
      • crm.objects.contacts.read - to find contacts by email
      • crm.objects.contacts.write - to associate notes with contacts
    • Copy the access token to your .env file

Granola credentials are automatically loaded from ~/Library/Application Support/Granola/supabase.json.

Usage

Test Connections

python3 -m src.main --test-granola
python3 -m src.main --test-hubspot

Dry Run

python3 -m src.main --dry-run

Full Sync

python3 -m src.main

Obtaining Granola Refresh Token

The refresh token and client_id are stored in supabase.json in Granola's application data directory.

Step 1: Login to Granola

Launch the Granola app and log in to create a valid session.

Step 2: Extract Refresh Token

View the file:

cat ~/Library/Application\ Support/Granola/supabase.json

The workos_tokens field contains a JSON string (escaped) with the tokens:

{
  "workos_tokens": "{\"access_token\":\"...\",\"refresh_token\":\"...\",\"token_type\":\"Bearer\",...}",
  "session_id": "...",
  "user_info": "{...}"
}

Extract refresh_token using jq:

cat ~/Library/Application\ Support/Granola/supabase.json | jq -r '.workos_tokens | fromjson | .refresh_token'

Or use Python:

python3 -c "import json; from pathlib import Path; print(json.loads(json.load(open(Path.home() / 'Library/Application Support/Granola/supabase.json'))['workos_tokens'])['refresh_token'])"

Step 3: Preserve Your Session (Important for Cloud Deployment)

To prevent the refresh token from expiring when Granola starts next time:

  1. Login to your Granola app (to generate a valid session)
  2. Extract the refresh token from supabase.json (as shown above)
  3. Quit Granola completely (Cmd+Q)
  4. Remove Application Support data to prevent session conflicts:
    rm -rf ~/Library/Application\ Support/Granola/

Why this works: Once you remove the Granola app data, the app can't invalidate your extracted refresh token when it starts up again. Your script will keep the token alive through continuous rotation.

Token Lifecycle

  • Refresh tokens are single-use only - each refresh invalidates the old token and returns a new one
  • Access tokens expire after 3600 seconds (1 hour)
  • The sync script automatically rotates tokens and keeps the session alive
  • If you don't rotate the refresh token regularly, it may expire and require re-extraction

Troubleshooting

Token already exchanged / Invalid grant:

  • The refresh token was already used and is now invalid
  • Re-authenticate to Granola and extract a fresh token from supabase.json
  • Follow Step 3 to preserve your session

Session keeps expiring:

  • Make sure the sync is running regularly (every ~5 minutes for cloud deployment)
  • Verify you removed the ~/Library/Application Support/Granola/ directory after extraction

Cloud Deployment (Railway)

Deploy to Railway so it runs automatically without your Mac:

1. Get your Granola refresh token

Follow the Obtaining Granola Refresh Token section above.

2. Deploy to Railway

  1. Push this repo to GitHub

  2. Go to railway.app and create a new project

  3. Select "Deploy from GitHub repo"

  4. Add environment variables:

    • HUBSPOT_ACCESS_TOKEN - your HubSpot token
    • GRANOLA_REFRESH_TOKEN - from step 1
    • SYNC_INTERVAL_SECONDS - how often to sync (default: 3600 = 1 hour)
  5. Change the start command to: python run_scheduled.py

3. Important: Token Rotation

Granola/WorkOS rotates refresh tokens. When the token refreshes, the new token is printed to logs:

[Token Refresh] New refresh token: xxxxx

You must update the GRANOLA_REFRESH_TOKEN env var in Railway with the new token, or the sync will stop working after the current token expires (~30 days).

Alternative: Local Cron

To run automatically on your Mac (must be awake):

crontab -e

Add:

0 * * * * cd /path/to/granola-connect && /usr/bin/python3 -m src.main >> ~/granola-sync.log 2>&1

How It Works

  1. Fetches meeting documents from Granola API
  2. Extracts attendee email addresses from each document
  3. Looks up matching contacts in HubSpot
  4. Converts meeting notes (enhanced notes) to HTML and creates a note in HubSpot
  5. Associates the note with all matching contacts
  6. Tracks synced documents in sync_state.json to avoid duplicates

Files

  • src/granola_client.py - Granola API client with token refresh
  • src/hubspot_client.py - HubSpot API client (Engagements API)
  • src/sync_state.py - State management
  • src/main.py - Main sync script
  • run_scheduled.py - Scheduled runner for cloud deployment
  • Dockerfile - Container configuration
  • railway.json - Railway deployment config

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages