---
name: pp-granola
description: "Granola notes, transcripts, audit events, and webhooks — plus offline SQLite cross-meeting search, attendee timelines, and a MEMO pipeline runner. Trigger phrases: `memo run for today's meetings`, `what's in granola but not yet memo'd`, `every meeting we had with trevin`, `verify granola webhook`, `list granola audit events`, `talk time in last week's meetings`, `extract granola meeting`, `use granola`, `run granola`."
author: "Damien Stevens"
license: "Apache-2.0"
argument-hint: "<command> [args] | install cli|mcp"
allowed-tools: "Read Bash"
metadata:
  openclaw:
    requires:
      bins:
        - granola-pp-cli
---
<!-- GENERATED FILE — DO NOT EDIT.
     This file is a verbatim mirror of library/productivity/granola/SKILL.md,
     regenerated post-merge by tools/generate-skills/. Hand-edits here are
     silently overwritten on the next regen. Edit the library/ source instead.
     See the repository agent guide, section "Generated artifacts: registry.json, cli-skills/". -->

<!-- // PATCH(dual-path-store-read): the Data Paths, Auth Setup, Auto-Refresh,
     and Troubleshooting sections describe the two-path reality after Granola
     desktop moved its data-encryption key into an entitlement-gated macOS
     keychain group. See library/productivity/granola/.printing-press-patches/
     dual-path-store-read.json and dek-migration-classified-from-state-not-version.json. -->

# Granola — Printing Press CLI

Created by [@dstevens](https://github.com/dstevens) (Damien Stevens).
Contributors: [@jeffreydebolt](https://github.com/jeffreydebolt) (Jeff DeBolt), [@mvanhorn](https://github.com/mvanhorn) (Matt Van Horn), [@giuseppebisemi](https://github.com/giuseppebisemi) (Giuseppe Bisemi), [@cathrynlavery](https://github.com/cathrynlavery) (Cathryn Lavery).

## Prerequisites: Install the CLI

This skill drives the `granola-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first:

1. Install via the Printing Press installer. It defaults binaries to `$HOME/.local/bin` on macOS/Linux and `%LOCALAPPDATA%\Programs\PrintingPress\bin` on Windows:
   ```bash
   npx -y @mvanhorn/printing-press-library install granola --cli-only
   ```
2. Verify: `granola-pp-cli --version`
3. Ensure the reported install directory is on `$PATH` for the agent/runtime that will invoke this skill.

If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.6 or newer):

```bash
go install github.com/mvanhorn/printing-press-library/library/productivity/granola/cmd/granola-pp-cli@latest
```

If `--version` reports "command not found" after install, the runtime cannot see the binary directory on `$PATH`. Do not proceed with skill commands until verification succeeds.

## Two Data Paths — Read This Before Running Anything

Granola desktop keeps its data-encryption key in a macOS data-protection keychain group gated by an entitlement bound to Granola's own Apple Team ID. No third-party binary can read that key, so the encrypted desktop cache (`cache-v6.json.enc`), the SQLCipher store `granola.db`, and `supabase.json.enc` are all unreadable by this CLI on a current install.

That does **not** leave the CLI without data. Run `granola-pp-cli auth login` once: the CLI signs in to Granola with its own session and syncs meetings over the API, no key and no paid workspace required. You approve one browser page; after that the session refreshes silently on every command. It never touches the Granola desktop app's session or your browser's.

Three paths fill the local SQLite store:

| Path | Hydrate with | Works on a current install? |
|---|---|---|
| CLI-owned session → local store | `auth login`, then `sync` | **Yes.** The default answer. Meetings, titles, timestamps, attendees. |
| Granola public REST API → local store | `sync-api` | **Yes**, but only with a `GRANOLA_API_KEY`, which needs a Business or Enterprise workspace. |
| Desktop encrypted cache → local store | `sync` | **Only on pre-migration builds**, or on a machine holding a pre-migration `storage.dek` supplied through `GRANOLA_SAFESTORAGE_KEY_OVERRIDE`. Otherwise `sync` runs degraded and reports so. |

**Reading already-synced data needs no credential at all.** Every read command serves from the local store first and falls back to the desktop cache only when the store has no row and a cache is actually readable. Neither step makes a network call or consults a key. Credentials are only needed to fetch *new* data.

### Capability split

Read this before telling a user their data is missing. An empty result on a migrated install usually means "not synced on this tier", not "you have none".

**With a CLI-owned session (`auth login`), hydrated by `sync`:**

- meetings, titles, timestamps
- attendees
- calendar events
- **transcripts** (full segment list)

Transcripts backfill incrementally. There is no bulk transcript endpoint, so `sync` fetches them one meeting at a time, newest first, up to `--transcript-budget` (default 250) per run. When work remains the command says so on stderr and records how many; re-run `sync` to continue, or pass `--transcript-budget -1` to fetch all remaining in one go. Meetings that genuinely have no recording are asked about once and then skipped forever.

**This matters when answering questions.** Before telling a user a meeting has no transcript, check whether the backfill has reached it. `sync` reports `transcripts_remaining` in its summary; a non-zero value means "not fetched yet", not "no transcript exists". Commands that depend on transcripts (`talktime`, `memo run`, `attendee brief`, `collect`) will be thin until the backfill completes.

- recipes and panel templates — `recipes list`, `recipes describe`
- folders and folder membership — `folder list`, `folder stream`

**Live on the same session, no sync needed:**

- AI panels — `panel get`, and the `--panel` inlining in `attendee brief` and `folder stream`
- workspaces — `workspaces list`

Both read straight from the API on each call, so they need no store rows. The tradeoff is that `panel get` is the one read command with no local fallback at all: if the session lapses it fails hard where everything else degrades to stored data.

**With a `GRANOLA_API_KEY`, hydrated by `sync-api`:** public-API notes, generated summaries, owner-only private notes when returned, folder membership, speaker attribution, and full transcripts. Long transcripts are retrieved through the paginated transcript endpoint when note detail returns HTTP 413.

**Frozen but still readable — AI chat threads (`chat list`, `chat get`):**

Chats are the one surface that cannot advance. Granola's internal API exposes no chat endpoint (seven namings probed on 7.465.0, 2026-08-03, all 404), so the threads in the store are whatever the last desktop-cache sync captured and no re-sync will add more. `chat list` says so in both its human and JSON output.

**Read the `staleness` block before answering from any of this.** Store-served reads carry one when the desktop cache is unreadable: `refreshable` tells you whether the surface can advance, `last_catalog_sync_at` dates refreshable surfaces like recipes, and `last_cache_sync_at` dates frozen ones like chats. A chat set can sit weeks behind the meetings it discusses — quote the date rather than presenting it as current.

When something in the first group is asked for on a migrated install, say the data is not reachable. Do not synthesize a panel, a recipe result, or a chat thread from transcript text.

### Why `auth login` when Granola desktop is already signed in

Because the desktop's session is not shareable. Its token lives behind the entitlement-gated key, and the refresh tokens the desktop and your browser hold are single-use — refreshing one signs *that* client out. So the CLI holds a chain of its own instead of borrowing. `auth login` stores it under the CLI's own data directory, readable only by you; `auth logout` removes it. Deleting it locally does not revoke it upstream.

## When to Use This CLI

Reach for granola-pp-cli when you need to answer cross-meeting questions Granola.ai’s web app and the GUI cannot — attendee timelines, MEMO pipeline state, recipes coverage gaps, calendar overlay, talk-time aggregation. It is the right tool for an agent processing transcripts in a loop, a CSM doing pre-call prep, or a consultant running a weekly retro. Pair the --json default with --select dotted paths to keep agent context lean.

## When Not to Use This CLI

Do not change remote state unless the user explicitly asks. The narrow remote mutation surface is `webhooks create / update / delete` and `meetings delete / restore`; webhook deletion requires `--yes`. Inspection, export, sync, audit reads, and offline webhook verification are safe read paths.

## Platform Notes

`warm <id> <query>` drives the Granola desktop GUI via AppleScript and is **macOS-only**. It prints what it would do by default; pass `--launch` to actually activate the app. On non-macOS hosts the command exits 0 with a "not supported" message. All other commands are cross-platform.

## Unique Capabilities

These capabilities aren't available in any other tool for this API.

### MEMO pipeline
- **`memo run`** — Run the preflight → extract pipeline on one meeting or every new meeting since a timestamp, emitting the MEMO three-file artifact and an ndjson run-state ledger.

  _Replaces the per-meeting shell loop that drives the MEMO pipeline — one call, one ndjson stream, agent-readable._

  ```bash
  granola-pp-cli memo run --since 24h --to ~/Documents/Dev/meeting-transcripts --json
  ```
- **`memo queue`** — List every meeting whose transcript is in the cache but whose MEMO triple is not yet on disk.

  _Answers the daily question “what’s still un-MEMO’d?” without the user opening Granola at all._

  ```bash
  granola-pp-cli memo queue --since 7d --json
  ```

### Attendee intelligence
- **`attendee timeline`** — Every meeting with a given attendee, ordered oldest→newest, with title, date, folder, and recipe-applied flag per row.

  _Pre-call prep in one command; surfaces the conversation arc with a single person across months of meetings._

  ```bash
  granola-pp-cli attendee timeline alice@example.com --since 60d --json --select id,title,started_at,folder,recipes
  ```
- **`attendee brief`** — Pulls the last N meetings with an attendee and stitches together their real cached notes plus real AI panel summaries — no synthesis.

  _Eliminates the click-each-meeting copy-paste that account leads do before every external call._

  ```bash
  granola-pp-cli attendee brief alice@example.com --last 3 --panel action-items --json
  ```

### Folders + recipes
- **`folder stream`** — ndjson stream of every meeting in a Granola folder (resolved via documentLists + listRules) with notes and a named panel inlined.

  _Replaces the weekly retro workflow of opening a folder and copy-pasting each meeting’s summary into a spreadsheet._

  ```bash
  granola-pp-cli folder stream client-foo --panel summary --json
  ```
- **`recipes coverage`** — Surface meetings that did NOT have a named panel template/recipe applied within a date range.

  _Friday retro question “did I run the Discovery recipe on every new-prospect call?” answered in one row per gap._

  ```bash
  granola-pp-cli recipes coverage discovery --since 14d --json
  ```

### Transcript analytics
- **`talktime`** — Per-segment-source talk-time for one meeting — microphone (you) vs system (everyone else) in minutes.

  _Confidence column lets you grade transcript accuracy; mic vs system split is the input to “am I talking too much” retros._

  ```bash
  granola-pp-cli talktime 196037d9 --json
  ```
- **`talktime`** — Lifts the per-source talk-time aggregation across N meetings since a date — who-talked-most over time.

  _Time-defrag retro input that no per-meeting tool can produce._

  ```bash
  granola-pp-cli talktime --by participant --since 7d --json
  ```

### Cache-native data

Both of these originate in Granola desktop's own cache. `chat list` reads the threads a cache `sync` already hydrated into the local store, so it keeps answering on a migrated install — but nothing can advance that set, and the output says so along with the last-sync timestamp. `calendar overlay` reads calendar events, which `sync-api` hydrates, so it keeps working.

- **`chat list`** — List and dump Granola’s AI chat threads anchored to a meeting (entities.chat_thread + entities.chat_message in the cache).

  _Recovers the AI Q&A history a user has accumulated against a meeting — useful when chasing what you asked about an account weeks ago._

  ```bash
  granola-pp-cli chat list 196037d9 --json
  ```
- **`calendar overlay`** — Left-anti-join meetingsMetadata calendar events with documents.google_calendar_event to find calendared-but-not-recorded meetings.

  _Sarah’s Friday retro and Damien’s “what did I miss” sweep both reduce to this row-level diff._

  ```bash
  granola-pp-cli calendar overlay --week 2026-05-11 --missed-only --json
  ```

### Direct SQL against the local store

The local store is plain SQLite at `~/.local/share/granola-pp-cli/data.db`, and querying it directly is a supported pattern for questions the commands don't cover. Read the schema first instead of guessing column names — the two classic wrong guesses are `meetings.source` (the real column is `row_source`) and `folders.name` (the real column is `title`):

```bash
# Path, tables, and columns — the contract your SQL runs against.
# Reads the CLI's own store, WAL-current and without writing to it.
granola-pp-cli db schema

# Machine-readable, for scripts
granola-pp-cli db schema --json

# Then query with confidence
sqlite3 ~/.local/share/granola-pp-cli/data.db \
  "select row_source, count(*) from meetings group by row_source;"
```

The main tables: `meetings`, `attendees`, `transcript_segments`, `folders` + `folder_memberships`, `panel_templates`, `recipes` + `recipes_usage`, `chat_threads` + `chat_messages`, and `sync_state`. Since schema v4, `row_source` (= cache|api) marks provenance on the catalog tables too — folders, panel templates, recipes — not just meetings, and folders carry `description` and `is_favourited`. `db schema` is the authoritative list; this paragraph is the orientation. Treat the store as read-only from SQL — writes belong to `sync` and `sync-api`.

### Pipeline hygiene
- **`duplicates scan`** — Hash (title, date-bucket, attendee-email-set) across the cache and a meeting-transcripts repo to surface duplicates at scale.

  _Repos accumulate near-duplicate files when meetings are re-extracted; this returns the dupe groups for cleanup._

  ```bash
  granola-pp-cli duplicates scan --root ~/Documents/Dev/meeting-transcripts --json
  ```
- **`tiptap extract`** — Render documents[id].notes (TipTap JSON: headings, bullet_list, list_item, bold marks, paragraph_break) to canonical markdown instead of falling back to notes_plain.

  _The MEMO summary file’s quality is bounded by extractor fidelity; granola.py loses sub-list hierarchy and bold runs._

  ```bash
  granola-pp-cli tiptap extract 196037d9 --as markdown
  ```

## Command Reference

This CLI exposes 35+ commands. The full tree is too long to inline; ask the CLI for the canonical list:

```bash
granola-pp-cli --help                              # top-level commands
granola-pp-cli <command> --help                    # subcommands + flags
granola-pp-cli agent-context --json                # machine-readable command tree for agents
```

Quick orientation by group:

| Group | Commands | Purpose |
|-------|----------|---------|
| **MEMO pipeline** | `memo run`, `memo queue`, `preflight`, `extract` | Composed three-stream pipeline; reads cache + writes MEMO triple |
| **Meetings** | `meetings list`, `meetings get`, `meetings fetch-batch`, `meetings delete`, `meetings restore`, `show` | List/inspect/mutate meetings (delete/restore mutate via internal API) |
| **Streams** | `notes-show`, `panel get`, `transcript get`, `tiptap extract` | The three streams — human notes, AI panels, transcript — addressable separately |
| **Export** | `export`, `export-all` | Combined three-stream markdown export, single or bulk |
| **Cross-meeting analytics** | `attendee timeline`, `attendee brief`, `folder stream`, `recipes coverage`, `talktime`, `calendar overlay`, `stats frequency`, `stats duration`, `stats attendees`, `stats calendar`, `collect`, `duplicates scan`, `chat list`, `chat get` | Queries no per-meeting tool can answer |
| **Folders / recipes / workspaces** | `folders` (public-API), `folder list`, `folder stream`, `recipes list`, `recipes describe`, `recipes coverage`, `workspaces list` | Granola organizational entities |
| **Public-API mirrors** | `notes list`, `notes get`, `folders` | Typed Bearer-key endpoints |
| **Public-API administration** | `audit list`, `webhooks list`, `webhooks create`, `webhooks update`, `webhooks delete`, `webhooks verify` | Audit reads with a dedicated key; webhook lifecycle management; offline delivery verification |
| **Sync / system** | `sync`, `sync-api`, `doctor`, `auth setup`, `auth status`, `auth set-token`, `auth logout`, `which`, `agent-context`, `version`, `import` | Local store hydration (`sync-api` is the working path on current installs), auth, capability discovery, batch import |
| **GUI bridge** | `warm` (macOS only) | Drives Granola desktop app via AppleScript |

### Finding the right command

When you know what you want to do but not which command does it, ask the CLI directly:

```bash
granola-pp-cli which "<capability in your own words>"
```

`which` resolves a natural-language capability query to the best matching command from this CLI's curated feature index. Exit code `0` means at least one match; exit code `2` means no confident match — fall back to `--help` or use a narrower query.

## Recipes


### Daily MEMO loop

```bash
granola-pp-cli memo run --since 24h --to ~/Documents/Dev/meeting-transcripts --json
```

Process every new meeting since yesterday into the MEMO triple format and yield only the new artifacts.

### Pre-call attendee brief

```bash
granola-pp-cli attendee brief alice@example.com --last 3 --panel action-items --json --select meetings.title,meetings.started_at,panels.action_items
```

Pull the last three meetings with Trevin and only the title, date, and action-items panel content per meeting.

### Friday retro — missing recipes

```bash
granola-pp-cli recipes coverage discovery --since 14d --json
```

Surface every new-prospect call in the last fortnight that did not have the Discovery panel applied. Omit the slug to list coverage gaps across every panel template.

### Repo-wide duplicate scrub

```bash
granola-pp-cli duplicates scan --root ~/Documents/Dev/meeting-transcripts --json
```

Find duplicate-meeting clusters across the MEMO output repo for cleanup.

### Calendar-overlay missed-meeting sweep

```bash
granola-pp-cli calendar overlay --week 2026-05-11 --missed-only --json
```

Calendared meetings with no Granola recording — weekly accountability check.

## Auth Setup

### 1. Get an API key — needed only to fetch new data

API keys are created in **Granola desktop → Settings → Connectors → API keys**. Creating one **requires a Business or Enterprise Granola workspace**; personal and free workspaces cannot issue keys. Two scopes exist, `personal-notes` and `public-notes` — pick the narrowest one that covers the notes the user actually needs, which for a user reading their own meetings is `personal-notes`.

Export it as an environment variable:

```bash
export GRANOLA_API_KEY="grn_your_key_here"
```

Prefer the env var over persisting the key into `~/.config/granola-pp-cli/config.toml`. Backup and dotfile-sync tooling does not reliably preserve file modes, so a key written to a config file can end up world-readable inside a synced folder.

The base URL is `http