Skip to content

feat(granola): complete API coverage, note triggers, and connector validation - #6880

Merged
waleedlatif1 merged 4 commits into
stagingfrom
feat/granola-expansion
Aug 20, 2026
Merged

feat(granola): complete API coverage, note triggers, and connector validation#6880
waleedlatif1 merged 4 commits into
stagingfrom
feat/granola-expansion

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Granola's public API has 9 endpoints; we had 3. Adds the remaining 6: get transcript, list audit events, and create/list/update/delete webhook endpoint.
  • Adds 4 note triggers (generated, edited, access granted, all events). Granola now supports programmatic webhook registration, so Sim creates the endpoint on deploy scoped to that trigger's events and deletes it on undeploy — no manual setup. Deliveries are verified with the Standard Webhooks HMAC-SHA256 signature Granola returns on creation, and event_id is the idempotency key (Granola reuses it across retries).
  • Validation fixes to the already-shipped tools:
    • get_note silently dropped speaker.attribution ("me"/"them"), so transcripts gave no way to tell the note-taker from other participants.
    • A 413 surfaced as a bare status code; it now explains the transcript is too large inline and points at Get Transcript.
    • Note IDs are URL-encoded rather than interpolated raw.
    • Array outputs were declared as type: 'json' with properties, which describes an object — agents saw notes.title instead of notes[i].title. All 15 now use type: 'array' with items.
  • Connector: hasMore was ANDed with the cursor, so a hasMore: true response with no cursor was reported as a complete listing. The sync engine treats exactly that shape as truncated and blocks deletion reconciliation; masking it meant a partial page could be taken for the whole corpus and reconciliation would hard-delete the rest. Also aligns mimeType with the plain-text bytes the engine actually writes.
  • Base URL, auth headers, and status-aware error handling are shared runtime helpers. params/outputs stay literal per file so the docs generator still reads them.

Type of Change

  • New feature
  • Bug fix

Testing

Tested manually against the published OpenAPI spec (docs.granola.ai/api-reference/openapi.json), field by field.

Added tests: signature verification (replay and body-tamper rejection, multi-signature headers), event matching, subscription create/delete, the block/tool contract, all four connector listingCapped quadrants plus the truncation signal, and the list parser / PATCH body semantics. Each was mutation-checked — reverting the fix turns the test red.

bun run check:audits (30/30), bun run lint, check-block-registry, bun run type-check, and 6240 tests across tools, connectors, webhooks, triggers, and blocks all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 20, 2026 1:53am

Request Review

@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR Summary

Cursor Bugbot is generating a summary for commit 214d699. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR expands Granola API, trigger, and connector support while correcting output schemas, transcript handling, pagination completeness, and webhook lifecycle validation.

  • Adds transcript, audit-event, and webhook-endpoint tools.
  • Adds signed Granola note-event triggers with deployment-managed subscriptions.
  • Corrects connector reconciliation signals and plain-text MIME metadata.
  • Adds contract, webhook, connector, and API-shaping tests.

Confidence Score: 4/5

The PR is not yet safe to merge because endpoint registration can still create duplicate active Granola endpoints when execution stops after the external POST but before its ID is checkpointed.

Granola endpoint creation remains a non-idempotent external side effect separated from persistence of the returned ownership state, leaving the previously reported duplicate-registration failure outstanding.

Files Needing Attention: apps/sim/lib/webhooks/providers/granola.ts

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/providers/granola.ts Adds signature verification and managed subscription lifecycle, but endpoint creation remains non-idempotent across the external-create/checkpoint boundary.
apps/sim/lib/webhooks/providers/granola.test.ts Covers authentication, event matching, normalization, idempotency extraction, and endpoint cleanup behavior.
apps/sim/triggers/granola/webhook.ts Defines the all-note-events trigger and its normalized output contract.
apps/sim/connectors/granola/granola.ts Preserves Granola's truncation signal and aligns declared MIME type with stored plain-text content.
apps/sim/blocks/blocks/granola.ts Exposes the expanded Granola tool and trigger surfaces with parameter coercion and matching block contracts.
apps/sim/tools/granola/create_webhook_endpoint.ts Adds direct webhook-endpoint creation and maps the one-time signing secret into the documented tool result.

Sequence Diagram

sequenceDiagram
  participant Deploy as Deployment
  participant Sim as Sim webhook registration
  participant Granola as Granola API
  participant Store as Registration store
  Deploy->>Sim: Prepare trigger subscription
  Sim->>Granola: POST webhook endpoint
  Granola-->>Sim: Endpoint ID and signing secret
  Sim->>Store: Checkpoint provider configuration
  Granola->>Sim: Signed note event
  Sim->>Sim: Verify signature and deduplicate event_id
  Sim->>Deploy: Start workflow execution
  Deploy->>Sim: Undeploy trigger
  Sim->>Granola: DELETE endpoint by stored ID
Loading

Reviews (4): Last reviewed commit: "fix(granola): never recover an orphaned ..." | Re-trigger Greptile

Comment thread apps/sim/lib/webhooks/providers/granola.ts
…ixes

Granola's public API exposes nine endpoints; Sim implemented three. Adds the
remaining six and wires the new programmatic webhook-endpoint lifecycle into a
managed trigger.

Tools (6 new, 9 total):
- get_transcript, list_audit_events
- create/list/update/delete_webhook_endpoint

Triggers: note.generated, note.edited, note.access_granted, plus an all-events
trigger. The provider handler registers the Granola endpoint on deploy and
deletes it on undeploy, scoped to the trigger's own event names, and verifies
every delivery with the Standard Webhooks HMAC-SHA256 signature Granola returns
on creation. event_id is the idempotency key, which Granola reuses across
retries.

Validation fixes to the shipped tools:
- get_note dropped speaker.attribution ("me"/"them"); now surfaced
- a 413 on get_note now explains that the transcript is too large inline and
  points at get_transcript, instead of surfacing a bare status code
- note IDs are URL-encoded rather than interpolated raw
- base URL, auth headers, and status-aware error handling are shared runtime
  helpers; params/outputs stay literal per file so the docs generator still
  reads them

Tests cover signature verification (including replay and body-tamper
rejection), event matching, subscription create/delete, and the block/tool
contract — plus a guard that ids shared between the tool and trigger surfaces
seed the same default, since block state is keyed by id and last-wins.

The knowledge-base connector was validated against the spec and needed no
changes.
…, and docs

Findings from validation passes over the tools, trigger, and connector.

Tools — array outputs were declared as `type: 'json'` with `properties`, which
describes an object, not an array. Agents and the output picker therefore saw
`notes.title` instead of `notes[i].title`. All 15 array outputs (including the
pre-existing three tools) now use `type: 'array'` with `items`, matching the
2000+ other tool files. The audit event `data` field stays `json`; it is
genuinely free-form per the spec.

Connector — `hasMore` was ANDed with the cursor, so a `hasMore: true` response
with no cursor was reported as a complete listing. The sync engine treats
exactly that shape as truncated and sets `listingTruncated` to block deletion
reconciliation; masking it meant a partial first page could be taken for the
whole corpus and reconciliation would hard-delete every note past it. Granola
would have to violate its own contract to emit that shape, but the engine
already handles it and the connector was hiding the signal. Also aligns
mimeType with the `.txt`/text-plain bytes the engine actually writes (it was
the only connector of 101 claiming text/markdown).

Trigger — the setup instructions named a Granola settings path that does not
exist; the help center says Settings > Connectors > API keys in the desktop app.

Both list parsers now split commas inside array entries, so an array-wrapped
free-text value cannot be sent as one malformed identifier.

Block — `id`, `events`, and `hasMore` are produced by several operations but
their descriptions named only one, unlike `folders` which already documented
both meanings.

Adds connector tests pinning all four listingCapped quadrants and the
truncation signal, and tool tests for the list parser and the PATCH body's
per-field "omit means unchanged" semantics.
@waleedlatif1
waleedlatif1 force-pushed the feat/granola-expansion branch from e4c912d to 4eb7645 Compare August 20, 2026 01:35
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/webhooks/providers/granola.ts
Comment thread apps/sim/lib/webhooks/providers/granola.ts
…tion

Raised independently by both reviewers. The registration service only rolls
external state back when createSubscription *returns* — its rollback is guarded
on `preparedProviderConfig`, so a handler that throws is assumed to have left
nothing behind. Granola's handler broke that contract: when Granola accepted the
POST but the success body was missing `id` or `signing_secret` (including a body
that failed to parse and became `{}`), it threw with the endpoint already live.

Nothing then recorded an external id, so undeploy could not remove it, and
Granola kept delivering to a callback whose signature could never be verified —
duplicating on every deploy retry.

The handler now removes what it created before rethrowing, matching the pattern
grain's multi-hook create already uses. It deletes by id when Granola returned
one, and otherwise recovers the endpoint by matching the callback URL, which
also covers a connection that fails after the request reached Granola.
Endpoints whose URL was redacted to its origin are never matched — that
comparison could delete another workflow's endpoint on the same host. Cleanup is
best effort and never masks the original failure. A non-2xx is left alone, since
no endpoint was created.

Also folds the delete call shared with deleteSubscription into one helper.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/webhooks/providers/granola.ts Outdated
Comment thread apps/sim/lib/webhooks/providers/granola.ts Outdated
The previous commit's URL-based recovery was unsafe. A redeploy reuses the live
registration's `path`, so the candidate and the currently serving endpoint share
a callback URL — listing by that URL and deleting every match would remove the
live deployment's endpoint and silently stop a working trigger, which is worse
than the leak it was trying to prevent.

Cleanup is now keyed solely on the id Granola returned. When the success body
carries no id there is no way to tell the candidate's endpoint from the live
one, so it is left in place: a leaked endpoint produces unverifiable deliveries
that Granola disables on its own, whereas deleting the wrong one takes down live
traffic with no signal.

The 2xx-missing-signing-secret case this originally fixed still cleans up, since
that response does carry an id.

Adds a test asserting no lookup or delete is attempted when the response has no
id, so URL matching cannot be reintroduced unnoticed.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b46787b. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

On the remaining 4/5 point — the duplicate-endpoint window when execution stops after the external POST but before the id is checkpointed.

That one is real, but it is not fixable inside this handler, and I don't think it should be fixed in this PR:

  • Granola's API offers no idempotency key. POST /v1/webhook-endpoints accepts only url, scopes, events, and folder_ids, and the endpoint takes no headers for this. There is no way to make the create itself idempotent.
  • Reusing or pre-deleting an endpoint by callback URL is the exact hazard that was just fixed. A redeploy reuses the live registration's path, so the candidate and the serving endpoint share a URL. Reuse is impossible anyway: signing_secret is returned only at creation and can never be re-fetched, so an adopted endpoint could never have its deliveries verified.
  • The window is in shared infrastructure, not here. prepareStableWebhookCandidate performs the external create and only then checkpoints; closing the gap means checkpointing a pending-create marker before the call. That is registration-service.ts behavior shared by every provider implementing createSubscription (~12 of them today), and changing it from an integration PR would alter all their deploy paths without their own review.

Blast radius if it does occur: the orphaned endpoint delivers to the same Sim path but carries the previous signing secret, so verifyAuth rejects it with a 401. Granola retries with backoff and then disables that endpoint on its own. So it does not double-execute workflows and does not lose data — it self-heals, at the cost of a disabled-endpoint notice to the endpoint's creator.

Happy to open a separate PR for the pending-create checkpoint in registration-service.ts if that's wanted, since the fix belongs there and benefits every provider at once.

@waleedlatif1
waleedlatif1 merged commit 9f34676 into staging Aug 20, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the feat/granola-expansion branch August 20, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant