Skip to main content

Granola Transcript Archiver

Automated system to archive Granola meeting transcripts to a GitHub repository. The archiver fetches new documents from Granola's API, formats them as Markdown, and commits them to a dedicated archive repository with proper organization and metadata.

Features

  • Automatic archiving: Polls Granola API for new/updated documents
  • Smart state tracking: SQLite database prevents duplicate archives
  • Date-based organization: Files organized as YYYY/MM/YYYY-MM-DD-title.md
  • Rich metadata: YAML frontmatter with document details, attendees, timestamps
  • Scheduled execution: macOS launchd integration for automatic runs
  • Idempotent: Safe to run multiple times - only archives new/updated documents
  • Dry-run mode: Preview what would be archived without committing

Project Structure

granola-archiver/
├── archiver/               # Main package
│   ├── main.py            # Orchestration and CLI
│   ├── state_tracker.py   # SQLite state management
│   ├── granola_fetcher.py # Granola API wrapper
│   ├── markdown_formatter.py  # Markdown generation
│   ├── git_manager.py     # Git operations
│   └── models.py          # Pydantic models
├── state/                 # SQLite database (git-ignored)
├── scripts/               # Automation scripts
├── config.yaml            # Configuration
└── README.md

Prerequisites

  1. Python 3.13+
  2. uv: Fast Python package manager - Install uv
  3. Granola API access: The archiver uses granola-py-client
  4. Git repository: A separate GitHub repository for storing archives

Installation

1. Install uv (if not already installed)

curl -LsSf https://astral.sh/uv/install.sh | sh

2. Install the archiver

cd granola-archiver
uv sync

The granola-client dependency will be installed automatically from PyPI.

4. Create archive repository

Create a new GitHub repository for storing transcripts:

# Create repo on GitHub, then clone locally
gh repo create granola-transcripts --private
git clone https://github.com/yourusername/granola-transcripts.git ~/granola-transcripts

# Initialize with README
cd ~/granola-transcripts
echo "# Granola Meeting Transcripts Archive" > README.md
git add README.md
git commit -m "Initial commit"
git push origin main

5. Configure the archiver

Copy the example configuration and update paths:

cp config.yaml config.yaml.local
# Edit config.yaml with your archive repo path

Example config.yaml:

archive:
  repo_path: /Users/yourusername/granola-transcripts
  remote_name: origin
  default_branch: main

granola:
  auto_detect_token: true  # Uses ~/.granola/credentials.json

polling:
  interval_minutes: 30
  lookback_hours: 24  # On first run

filters:
  workspace_ids: []  # Empty = all workspaces
  min_duration_minutes: 0

logging:
  level: INFO
  file: /tmp/granola-archiver.log

Usage

Manual Execution

# Normal run - archive new documents
uv run archiver

# Dry run - preview what would be archived
uv run archiver --dry-run

# Archive a specific document
uv run archiver --document-id doc_abc123

# Use custom config file
uv run archiver --config /path/to/config.yaml

# Backfill: archive ALL historical documents
uv run archiver --backfill

# Backfill with dry-run to preview
uv run archiver --backfill --dry-run

# Archive documents since a specific date
uv run archiver --since 2024-01-01

# Archive documents since a specific datetime
uv run archiver --since 2024-01-01T10:00:00

Automatic Execution (macOS)

Set up a launchd job to run the archiver every 30 minutes:

./scripts/setup_launchd.sh

This creates ~/Library/LaunchAgents/com.granola.archiver.plist and loads it.

Useful commands:

# View status
launchctl list | grep granola

# View logs
tail -f /tmp/granola-archiver.log

# View errors
tail -f /tmp/granola-archiver.error.log

# Unload job
launchctl unload ~/Library/LaunchAgents/com.granola.archiver.plist

# Reload job (after config changes)
launchctl unload ~/Library/LaunchAgents/com.granola.archiver.plist
launchctl load ~/Library/LaunchAgents/com.granola.archiver.plist

Archive Format

Documents are archived as Markdown with YAML frontmatter:

---
title: "Team Standup"
date: 2026-01-30T14:00:00Z
document_id: doc_abc123
workspace_id: ws_engineering
created_at: 2026-01-30T14:00:00Z
updated_at: 2026-01-30T15:30:00Z
archived_at: 2026-01-30T16:00:00Z
attendees:
  - name: "Alice"
    email: "alice@example.com"
  - name: "Bob"
    email: "bob@example.com"
---

# Team Standup

**Date**: January 30, 2026
**Attendees**: Alice, Bob

## Overview

Quick daily standup to sync on project progress.

## Transcript

**[00:00:00]** Alice: Good morning everyone...

## Notes

- Alice: Working on feature X
- Bob: Investigating bug Y

---
*Archived: 2026-01-30*

Files are organized by date:

granola-transcripts/
├── 2026/
│   ├── 01/
│   │   ├── 2026-01-30-team-standup.md
│   │   ├── 2026-01-30-client-meeting.md
│   │   └── 2026-01-31-brainstorm-session.md
│   └── 02/
│       └── 2026-02-01-quarterly-review.md

How It Works

  1. State Check: Reads last run timestamp from SQLite database
  2. Fetch Documents: Queries Granola API for new/updated documents since last run
  3. Filter: Skips already-archived documents (checks document ID + updated_at)
  4. Process Each Document:
    • Fetch full details (transcript, metadata)
    • Format as Markdown with YAML frontmatter
    • Compute file path based on creation date
    • Write file and create git commit
    • Mark as archived in database
  5. Push: Push all commits to remote repository
  6. Update State: Record run statistics and timestamp

Configuration Options

Granola API

granola:
  auto_detect_token: true  # Auto-detect from ~/.granola/credentials.json
  token_env: GRANOLA_TOKEN  # Or use environment variable

Filtering

filters:
  workspace_ids: ["ws_eng", "ws_product"]  # Specific workspaces only
  min_duration_minutes: 5  # Skip meetings shorter than 5 minutes

Polling

polling:
  interval_minutes: 30  # How often launchd runs (in setup script)
  lookback_hours: 24  # On first run, how far back to look

State Management

The archiver maintains state in state/archive_state.db (SQLite):

archived_documents table:

  • Tracks which documents have been archived
  • Prevents duplicate archives
  • Records file paths and commit SHAs

archive_runs table:

  • Logs each archiver run
  • Tracks success/failure statistics
  • Used to determine last successful run time

To reset state and re-archive everything:

rm state/archive_state.db

Error Handling

The archiver is designed to be resilient:

  • Authentication failures: Aborts run immediately with clear error
  • API errors: Logs error, skips document, continues with others
  • Git conflicts: Logs error, skips document, continues
  • Network issues: Handled by granola-client's retry logic

Errors are logged to both console and /tmp/granola-archiver.log.

Troubleshooting

"Configuration file not found"

Make sure config.yaml exists in the working directory or specify path with --config.

"Repository path does not exist"

The archive repository path in config.yaml must point to an existing git repository.

"granola-client not found"

Reinstall dependencies:

uv sync

"Authentication failed"

Ensure Granola credentials exist at ~/.granola/credentials.json or set GRANOLA_TOKEN environment variable.

Launchd job not running

Check if loaded:

launchctl list | grep granola

Check error logs:

tail -f /tmp/granola-archiver.error.log

Development

Running tests

uv sync --all-extras
uv run pytest

Code formatting

uv run black archiver/
uv run ruff check archiver/

Future Enhancements

Claude API Integration (Optional)

The plan includes an optional enhancement plugin to improve notes using Claude API:

# Future feature - not yet implemented
from archiver.enhancement_plugin import ClaudeEnhancementPlugin

enhancer = ClaudeEnhancementPlugin()
enhanced_markdown = await enhancer.enhance(document, markdown)

This would:

  • Extract action items clearly
  • Highlight key decisions
  • Improve formatting
  • Generate executive summaries

To enable, add to config:

enhancement:
  enabled: true
  model: claude-sonnet-4-5-20250929

License

MIT

Contributing

Contributions welcome! Please open an issue or pull request.

Metadata

Release files for granola-archiver 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for granola-archiver 0.4.0
File Size Uploaded
granola_archiver-0.4.0.tar.gz 46.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for granola-archiver 0.4.0
File Interpreter ABI Platform
granola_archiver-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 64.9 kB

Release files / granola_archiver-0.4.0.tar.gz

Download URL granola_archiver-0.4.0.tar.gz
Size 46.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f68c1e45a0eb0a7fab896e7a30dcb75457a53cfddcb14449671be2261aa7ca00
BLAKE2b-256 checksum
How to use checksums
b766bdc4a44c4c2a34a628fdeac5a96d66ee0729bd2ed1a64de13ce2fe949126
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 2, 2026.

Transparency log

Release files / granola_archiver-0.4.0-py3-none-any.whl

Download URL granola_archiver-0.4.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
737adbea9163ede137a3cb20416b5ff5750aa5129228049a2b8ce52955bb2ad6
BLAKE2b-256 checksum
How to use checksums
c1bcdc5bc512ed072f3a2181231928160082b761a1e3862110fb1f9594bd2bb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page