This document explains how the Granola Sync plugin synchronizes notes and transcripts from Granola to Obsidian.
The sync process retrieves documents and transcripts from the Granola API and saves them to the Obsidian vault. The plugin supports multiple sync destinations and handles deduplication through Granola ID tracking.
flowchart TD
A[Sync Triggered] --> B[Load Credentials]
B --> C{Credentials Loaded?}
C -->|No| D[Show Error Notice]
C -->|Yes| E[Build Granola ID Cache]
E --> F[Fetch Documents from API]
F --> G{Documents Found?}
G -->|No| H[End Sync]
G -->|Yes| I{Transcripts Enabled?}
I -->|Yes| J[Sync Transcripts]
I -->|No| K[Skip Transcripts]
J --> L{Notes Enabled?}
K --> L
L -->|Yes| M[Sync Notes]
L -->|No| H
M --> O[Update Last Sync Time]
O --> P[Update Status Bar]
P --> H
The plugin loads credentials by reading directly from the filesystem. This approach provides secure access to credentials stored in the Granola application's data directory without requiring a temporary HTTP server.
sequenceDiagram
participant Plugin
participant CredentialsService
participant FileSystem
participant GranolaApp
Plugin->>CredentialsService: loadCredentials()
CredentialsService->>FileSystem: Read {OS-specific}/supabase.json
FileSystem-->>CredentialsService: JSON Content or Error
alt File Read Success
CredentialsService->>CredentialsService: Parse JSON
CredentialsService->>CredentialsService: Extract workos_tokens.access_token
CredentialsService->>CredentialsService: Check Token Expiration
alt Token Expired
CredentialsService->>GranolaApp: Refresh Token via API
GranolaApp-->>CredentialsService: New Access Token
end
CredentialsService-->>Plugin: { accessToken, error: null }
else File Read Error
CredentialsService->>CredentialsService: Generate Error Message
CredentialsService-->>Plugin: { accessToken: null, error }
end
- File Location:
{OS-specific}/supabase.json- Windows:
%APPDATA%/Granola/supabase.json - Linux:
~/.config/Granola/supabase.json - macOS:
~/Library/Application Support/Granola/supabase.json
- Windows:
- Token Path:
workos_tokens.access_tokenwithin the JSON structure - Token Refresh: Automatically refreshes expired tokens using the refresh token
- Error Handling: Provides clear error messages for:
- File not found (ENOENT)
- Permission errors (EACCES)
- Invalid JSON format
- Missing required fields
The plugin fetches documents from the Granola API using the loaded access token.
sequenceDiagram
participant Plugin
participant GranolaAPI
participant GranolaServer
Plugin->>GranolaAPI: fetchGranolaDocuments(accessToken)
GranolaAPI->>GranolaServer: POST /v2/get-documents
Note over GranolaAPI: Headers: Authorization Bearer token<br/>Body: {limit: 100, offset: 0,<br/>include_last_viewed_panel: true}
GranolaServer-->>GranolaAPI: {docs: GranolaDoc[]}
GranolaAPI->>GranolaAPI: Validate Response Structure
GranolaAPI-->>Plugin: GranolaDoc[]
Each GranolaDoc contains:
id: Unique Granola document identifiertitle: Document titlecreated_at: Creation timestamp (optional)updated_at: Last update timestamp (optional)last_viewed_panel.content: ProseMirror document structure (optional)notes_markdown: Raw private notes in markdown format (optional)
The plugin handles various HTTP error codes:
- 401: Authentication failed (token expired)
- 403: Access forbidden
- 404: API endpoint not found
- 500+: Server errors
- Other: Network or connection errors
Before syncing, the plugin builds a cache mapping Granola IDs to existing Obsidian files. This enables efficient deduplication.
flowchart LR
A[Scan All Markdown Files] --> B[Read Frontmatter]
B --> C{Has granola_id?}
C -->|Yes| D[Add to Cache Map]
C -->|No| E[Skip File]
D --> F[Next File]
E --> F
F --> G{More Files?}
G -->|Yes| B
G -->|No| H[Cache Ready]
Cache Structure: Map<cacheKey, TFile>
- Key: Composite key in format
{granolaId}-{type}where type is 'note' or 'transcript' - Value: Obsidian
TFilereference - This allows notes and transcripts with the same Granola ID to coexist without conflicts
All synced files include frontmatter with metadata for tracking and identification.
---
granola_id: doc-123
title: "Meeting Title"
type: note
created: 2024-01-15T10:00:00Z
updated: 2024-01-15T12:00:00Z
------
granola_id: doc-123
title: "Meeting Title - Transcript"
type: transcript
created: 2024-01-15T10:00:00Z
updated: 2024-01-15T12:00:00Z
---Key Features:
granola_id: Consistent across both note and transcript for the same source documenttype: Distinguishes between 'note' and 'transcript' filescreatedandupdated: Timestamps from Granola API (when available)- Both file types can share the same
granola_idwhile being uniquely identified bytype
The plugin automatically migrates legacy frontmatter formats on load:
- Removes
-transcriptsuffix fromgranola_idin transcript files - Adds
typefield to all files (note/transcript) - Adds missing timestamps to transcript files (when available)
- Migration runs silently in the background
- Will be removed in version 2.0.0
Notes can be synced to three different destinations, each with different behavior:
flowchart TD
A[Sync Notes] --> B{Destination Type?}
B -->|Daily Notes| C[Group by Date]
B -->|Daily Note Folder Structure| D[Individual Files]
B -->|Granola Folder| D
C --> E[Create/Update Daily Note]
E --> F[Update Section with Heading]
D --> G[For Each Document]
G --> H[Convert ProseMirror to Markdown]
H --> H1{Include Private Notes?}
H1 -->|Yes & Has Content| H2[Add Private Notes + Enhanced Notes Sections]
H1 -->|No or Empty| H3[Add Note Content Only]
H2 --> I[Add Frontmatter]
H3 --> I
I --> J[Save to Disk]
When syncing to daily notes, documents are grouped by date and inserted into sections within daily note files.
flowchart TD
A[Documents List] --> B[For Each Document]
B --> C[Extract Date from document timestamps]
C --> D[Group by Date YYYY-MM-DD]
D --> E[Convert ProseMirror to Markdown]
E --> E1{Include Private Notes?}
E1 -->|Yes & Has Content| E2[Add Private Notes Section]
E1 -->|No or Empty| E3[Skip Private Notes]
E2 --> F[Add to Daily Notes Map]
E3 --> F
F --> G{More Documents?}
G -->|Yes| B
G -->|No| H[For Each Date Group]
H --> I[Get/Create Daily Note File]
I --> J[Build Section Content]
J --> K[Update Section with Heading]
K --> L{More Dates?}
L -->|Yes| H
L -->|No| M[Complete]
Section Format:
- Heading: User-configured section heading (trimmed)
- Each note includes:
- H3 title
- Granola ID
- Created/Updated timestamps
- Optional transcript link
- Note content (with optional Private Notes and Enhanced Notes sections if enabled)
Private Notes Support:
When the "Include Private Notes" setting is enabled and notes_markdown has content:
- Notes include a "## Private Notes" section at the top with the raw private notes
- Followed by a "## Enhanced Notes" section with the processed note content
- When disabled or
notes_markdownis empty, notes display content directly without section headings
The saveToDisk method handles file creation, updates, and deduplication.
flowchart TD
A[saveToDisk Called] --> B[Determine Folder Path]
B --> C{File Type?}
C -->|Transcript| D[Use Transcript Destination]
C -->|Note| E[Use Note Destination]
D --> F[Ensure Folder Exists]
E --> F
F --> G{Has granolaId?}
G -->|Yes| H[Search Cache for Existing File]
G -->|No| I[Search by File Path]
H --> J{File Found?}
I --> J
J -->|Yes| K[Update Existing File]
J -->|No| L[Create New File]
K --> M{Path Changed?}
M -->|Yes| N[Attempt Rename]
M -->|No| O[Update Cache]
N --> O
L --> O
O --> P[Return Success]
- Primary: Search by Granola ID in cache
- Fallback: Search by computed file path
- Update: If file exists, update content
- Rename: If path changed (e.g., title changed), rename file
- Cache Update: Always update cache after save
Path computation depends on the destination type:
Daily Note Folder Structure:
- Uses daily note format settings
- Extracts folder structure from date format
- Combines with base daily notes folder
Granola Folder / Granola Transcripts Folder:
- Uses configured folder path directly
- Normalizes path separators
Filename Sanitization:
- Removes invalid characters:
<>:"/\|?* - Replaces spaces with underscores
- Truncates to 200 characters maximum
The sync process orchestrates multiple components:
- Credentials Management: Direct filesystem reading with automatic token refresh
- API Communication: Fetching documents and transcripts from Granola API
- Deduplication: Using Granola ID cache to prevent duplicates
- Content Conversion: ProseMirror to Markdown transformation
- File Management: Creating/updating files in appropriate locations
- Error Handling: Comprehensive error reporting and recovery
The plugin supports flexible sync destinations and maintains data integrity through ID-based deduplication and careful file management.