Skip to content

Implement Granola Sync #4

Description

@aaronkwhite

Summary

Add integration with Granola for syncing meeting notes to local markdown files.

Prerequisites

Research Findings (Feb 2026)

API Status

Granola does not offer a public API. Integration uses local auth tokens + reverse-engineered endpoints.

Reference Implementation

The Granola-to-Obsidian plugin provides a complete working example.

Technical Details

Authentication:

  • Read WorkOS tokens from ~/Library/Application Support/Granola/supabase.json (macOS)
  • Tokens use OAuth 2.0 with single-use rotating refresh tokens
  • Each token refresh invalidates the previous token

API Endpoints:

POST https://api.granola.ai/v2/get-documents        # List documents (paginated)
POST https://api.granola.ai/v1/get-document-transcript   # Get transcript
POST https://api.granola.ai/v1/get-documents-batch       # Batch fetch by IDs
POST https://api.granola.ai/v2/get-document-lists        # List folders
POST https://api.granola.ai/v1/get-workspaces            # List workspaces

Content Format:

  • Granola uses ProseMirror internally (same as PM Toolkit's Tiptap!)
  • Can convert directly between formats or serialize to Markdown

Implementation Plan

Phase 1: Core Infrastructure

1.1 Granola Detection

  • Check for Granola installation (look for supabase.json)
  • Show "Granola not detected" state in settings if missing
  • Platform-specific paths (macOS, Windows, Linux)

1.2 Token Management

  • Create src/granola/TokenManager.ts
  • Read tokens from local auth file
  • Handle token refresh with rotation
  • Secure token storage in extension context

1.3 API Client

  • Create src/granola/GranolaClient.ts
  • Implement getDocuments() with pagination
  • Implement getDocumentTranscript()
  • Implement getDocumentsBatch()
  • Implement getFolders()
  • Implement getWorkspaces()
  • Error handling and retry logic

Phase 2: Content Sync

2.1 Document Conversion

  • Create src/granola/ContentConverter.ts
  • ProseMirror → Markdown conversion
  • Transcript → Markdown formatting
  • YAML frontmatter generation with granola_id

2.2 File Management

  • Create src/granola/SyncManager.ts
  • Configurable sync directory
  • Filename template system (like Obsidian plugin)
  • Duplicate detection via granola_id frontmatter
  • Date-based folder organization (optional)
  • Granola folder mirroring (optional)

2.3 Sync Operations

  • Manual sync command: pmtoolkit.syncGranola
  • Status bar indicator during sync
  • Progress notifications
  • Sync summary (X notes synced)

Phase 3: Auto-Sync & Watching

3.1 Scheduled Sync

  • Configurable sync interval (1min to daily)
  • Background sync with minimal UI interruption
  • Last sync timestamp tracking

3.2 File Watching (Optional)

  • Watch Granola cache directory for changes
  • Trigger sync on new meeting detected
  • Debounce rapid changes

Phase 4: Settings UI

Add "Granola" section to PM Toolkit Settings panel:

Connection

  • Status indicator (Connected / Not Detected / Error)
  • "Sync Now" button
  • Last synced timestamp

Sync Settings

  • Enable/disable auto-sync toggle
  • Sync frequency dropdown (1min, 5min, 15min, 30min, 1hr, daily, manual only)
  • Sync directory picker
  • Document limit (when not syncing all)
  • Sync all historical notes toggle

Content Settings

  • Include transcript toggle
  • Include "My Notes" toggle
  • Include "Enhanced Notes" (AI summary) toggle

File Organization

  • Filename template input with variables: {title}, {created_date}, {id}
  • Date format dropdown
  • Enable date-based folders toggle
  • Enable Granola folder mirroring toggle
  • Word separator dropdown (underscore, hyphen, space)

Filtering

  • Enable folder filter toggle
  • Folder multi-select (sync only selected Granola folders)
  • Workspace selector (for multi-workspace users)

Advanced

  • Custom auth file path (override default)
  • Existing file behavior (skip / create timestamped version)
  • Debug logging toggle

Settings Schema

{
  "pmtoolkit.granola.enabled": false,
  "pmtoolkit.granola.syncDirectory": "Granola",
  "pmtoolkit.granola.autoSyncFrequency": 300000,
  "pmtoolkit.granola.filenameTemplate": "{created_date}_{title}",
  "pmtoolkit.granola.dateFormat": "YYYY-MM-DD",
  "pmtoolkit.granola.includeTranscript": false,
  "pmtoolkit.granola.includeMyNotes": true,
  "pmtoolkit.granola.includeEnhancedNotes": true,
  "pmtoolkit.granola.enableDateFolders": false,
  "pmtoolkit.granola.enableGranolaFolders": false,
  "pmtoolkit.granola.documentSyncLimit": 100,
  "pmtoolkit.granola.syncAllHistorical": false,
  "pmtoolkit.granola.selectedFolders": [],
  "pmtoolkit.granola.existingFileAction": "skip"
}

File Structure

src/
├── granola/
│   ├── index.ts              # Exports
│   ├── TokenManager.ts       # Auth token handling
│   ├── GranolaClient.ts      # API client
│   ├── ContentConverter.ts   # ProseMirror → Markdown
│   ├── SyncManager.ts        # File sync orchestration
│   └── types.ts              # Granola data types

Commands

Command Description
pmtoolkit.syncGranola Manually trigger Granola sync
pmtoolkit.openGranolaSettings Jump to Granola section in settings

Status Bar

When Granola is enabled, show status in bottom bar:

  • $(sync~spin) Syncing Granola... - during sync
  • $(check) Granola: 5 synced - after successful sync (3s)
  • $(error) Granola: Error - on failure
  • $(cloud) Granola - idle (clickable to sync)

Error Handling

Error User Message Action
Auth file not found "Granola not installed or not signed in" Show setup instructions
Token expired "Granola session expired" Prompt to open Granola app
API error "Granola sync failed: {error}" Retry with backoff
Network error "Unable to reach Granola servers" Queue for retry

Testing

  • Unit tests for TokenManager
  • Unit tests for ContentConverter
  • Integration tests for API client (mocked)
  • E2E test for full sync flow

Risks & Mitigations

Risk Mitigation
API changes break sync Version detection, graceful degradation
Token rotation fails Clear guidance to re-auth via Granola app
Large sync overwhelms system Pagination, progress indicator, cancellation
Conflicts with existing files Configurable behavior, never overwrite without consent

Sources

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requeston-holdBlocked or deferred

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions