This is a Granola.ai meetings export script best used
to daily incrementally back up meetings to your GitHub. The exporter writes
generated artifacts into the granola-backups branch. Locally, artifacts are
stored under backups/, but ignored on main by default.
This is an unofficial exporter that relies on Granola API behavior observed from the desktop app. It is not affiliated with or endorsed by Granola.
For each meeting, it exports:
notes.mdandnotes.jsonenhanced.mdandenhanced.jsontranscript.mdandtranscript.jsonmeeting.json(metadata snapshot)
Meeting folder naming:
YYYYMMDD_Title(example:20260303_Allie)- if two meetings collide on same date/title, exporter appends
--<id8>
This repository is designed to be copied into your own private GitHub repo. When used as a GitHub template, the workflow and exporter code are copied, but your GitHub Secrets and backup data are not.
Recommended setup:
- Create a new private repo from this template.
- Add the
GRANOLA_SUPABASE_JSONrepository secret. - Run the
Granola Backupworkflow manually once from the Actions tab. - Keep
mainfor code and config. - Let the workflow publish generated backups to the
granola-backupsbranch.
The workflow is intentionally included in the template. If it runs before
GRANOLA_SUPABASE_JSON is configured, it exits successfully with a setup notice
and does not create backup files or branches.
Exported backups contain private meeting content, transcripts, notes, metadata,
and AI summaries. Treat the backup branch and any local backups/ folder as
sensitive data.
- Keep repos containing real backups private.
- Do not commit
.env,supabase.json, access tokens, or refresh tokens. - Do not make a repo public if its Git history ever contained real backups.
- For public sharing, create a fresh repo with clean history and no generated backup files.
scripts/export_granola.py: main exporter.github/workflows/granola-backup.yml: daily workflowbackup.config.yaml: runtime configbackups/: generated output artifacts and manifests, ignored by Git
Export paths:
backups/granola-md/<meeting-folder>/...backups/granola-json/<meeting-folder>/...backups/manifests/...
backups/is ignored in.gitignore.- Existing backup files have been removed from Git tracking with
git rm --cached; the files can still exist locally. - Moving
backups/out of this repo will not remove tracked files from Git, because it is not currently tracked. - The exporter always recreates
backups/inside the repo on the next run. Ifbackups/manifests/sync_state.jsonis moved away, the next run has no incremental marker and behaves like a first export.
The scheduled workflow runs from main, using the latest exporter code from
main, but publishes generated backup files to a separate granola-backups
branch.
maincontains code, config, docs, and workflow files.granola-backupscontains generatedbackups/output only.- Before each export, the workflow restores
backups/fromgranola-backupsif the branch exists, so incremental state is preserved inbackups/manifests/sync_state.json. - After each export, the workflow replaces the backup branch contents with the newly generated
backups/tree. - Code changes only need to be made on
main; the next scheduled run automatically uses them.
GRANOLA_SUPABASE_JSON: full contents of your Granolasupabase.json
On macOS, this file is typically at:
~/Library/Application Support/Granola/supabase.json
GRANOLA_CLIENT_ID: override client id (default:client_GranolaMac)GRANOLA_API_BASE: override API base (default:https://api.granola.ai)BACKUP_WORKSPACE_ID: restrict backup to a single workspaceALLOW_LARGE_DROP: set totrueto bypass health guard if meeting count drops unexpectedlyALLOW_EMPTY_CONTENT_OVERWRITE: set totrueto allow empty/null API responses to overwrite existing non-empty export files
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# one-time setup
cp .env.example .env
# then paste your real supabase.json content into GRANOLA_SUPABASE_JSON in .env
# run (script auto-loads .env)
python scripts/export_granola.pyThe workflow can run on a schedule or manually from the Actions tab. It expects
the GRANOLA_SUPABASE_JSON repository secret to be present. Without that
secret, the workflow skips cleanly and prints a setup notice.
To add the secret:
- Open your GitHub repo.
- Go to Settings -> Secrets and variables -> Actions.
- Add a new repository secret named
GRANOLA_SUPABASE_JSON. - Paste the full contents of your local Granola
supabase.jsonfile.
The workflow is scheduled with:
cron: "0 2 * * *"You can change that cron expression in .github/workflows/granola-backup.yml.
Generated backups are published to granola-backups, not main, so the default
branch stays focused on code, config, docs, and workflow files.
- Local terminal (
PROGRESS_MODE=auto, default): shows spinner/progress and phase updates. - GitHub Actions (
GITHUB_ACTIONS=true): progress UI is suppressed by default.
Optional controls:
PROGRESS_MODE=auto|rich|plain|offPROGRESS_LOG_EVERY=25(for plain mode update cadence)
State is stored in backups/manifests/sync_state.json.
- First run exports all meetings.
- Later runs export only meetings with
updated_atnewer than the last successful sync marker. - The run manifest records per-file statuses, content counts, missing-content warnings, and skipped empty overwrites.
Use FULL_EXPORT=true to force a full re-export.
notes.mdcontains manual notes typed into Granola. It can be empty even when a transcript exists.enhanced.mdcontains Granola's AI-generated summary from the document panel.- The exporter skips overwriting existing non-empty export files with empty/null content unless
ALLOW_EMPTY_CONTENT_OVERWRITE=trueis set.