From 67492a67830f1bdf8f2d0014957bb8bdacb23195 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 13:23:23 -0700 Subject: [PATCH 1/4] feat(granola): add Granola meeting notes integration --- apps/docs/components/icons.tsx | 11 ++ apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/granola.mdx | 103 +++++++++++ apps/docs/content/docs/en/tools/meta.json | 1 + .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 27 +++ apps/sim/blocks/blocks/granola.ts | 163 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 11 ++ apps/sim/tools/granola/get_note.ts | 156 +++++++++++++++++ apps/sim/tools/granola/index.ts | 5 + apps/sim/tools/granola/list_notes.ts | 122 +++++++++++++ apps/sim/tools/granola/types.ts | 53 ++++++ apps/sim/tools/registry.ts | 3 + 14 files changed, 661 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/granola.mdx create mode 100644 apps/sim/blocks/blocks/granola.ts create mode 100644 apps/sim/tools/granola/get_note.ts create mode 100644 apps/sim/tools/granola/index.ts create mode 100644 apps/sim/tools/granola/list_notes.ts create mode 100644 apps/sim/tools/granola/types.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 0cf5773ca48..cd0c085f913 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -5091,6 +5091,17 @@ export function GrainIcon(props: SVGProps) { ) } +export function GranolaIcon(props: SVGProps) { + return ( + + + + ) +} + export function CirclebackIcon(props: SVGProps) { const id = useId() const patternId = `circleback_pattern_${id}` diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 900229eaad7..5563108d3b8 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -74,6 +74,7 @@ import { GoogleVaultIcon, GrafanaIcon, GrainIcon, + GranolaIcon, GreenhouseIcon, GreptileIcon, HexIcon, @@ -247,6 +248,7 @@ export const blockTypeToIconMap: Record = { google_vault: GoogleVaultIcon, grafana: GrafanaIcon, grain: GrainIcon, + granola: GranolaIcon, greenhouse: GreenhouseIcon, greptile: GreptileIcon, hex: HexIcon, diff --git a/apps/docs/content/docs/en/tools/granola.mdx b/apps/docs/content/docs/en/tools/granola.mdx new file mode 100644 index 00000000000..9bdefed216d --- /dev/null +++ b/apps/docs/content/docs/en/tools/granola.mdx @@ -0,0 +1,103 @@ +--- +title: Granola +description: Access meeting notes and transcripts from Granola +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Granola](https://www.granola.ai/) is an AI-powered meeting notes platform that automatically captures, transcribes, and summarizes your meetings. Granola runs quietly in the background during calls, producing structured notes with key takeaways so you never miss important details. + +With the Granola integration in Sim, you can: + +- **List meeting notes**: Retrieve your recent meeting notes with flexible date and pagination filters +- **Get note details**: Access the full summary, attendees, calendar event metadata, and folder membership for any note +- **Include transcripts**: Optionally retrieve the complete speaker-attributed transcript for a meeting +- **Filter by date**: Narrow results to notes created before/after a date or updated after a date + +In Sim, the Granola integration enables your agents to programmatically access meeting intelligence as part of their workflows. This allows for automation scenarios such as extracting action items from meetings, syncing meeting summaries to other tools, building meeting digest reports, or triggering follow-up tasks based on meeting content. +{/* MANUAL-CONTENT-END:intro */} + +## Usage Instructions + +Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts. + +## Tools + +### `granola_list_notes` + +Lists meeting notes from Granola with optional date filters and pagination. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Granola API key | +| `createdBefore` | string | No | Return notes created before this date \(ISO 8601\) | +| `createdAfter` | string | No | Return notes created after this date \(ISO 8601\) | +| `updatedAfter` | string | No | Return notes updated after this date \(ISO 8601\) | +| `cursor` | string | No | Pagination cursor from a previous response | +| `pageSize` | number | No | Number of notes per page \(1-30, default 10\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `notes` | json | List of meeting notes | +| ↳ `id` | string | Note ID | +| ↳ `title` | string | Note title | +| ↳ `ownerName` | string | Note owner name | +| ↳ `ownerEmail` | string | Note owner email | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| `hasMore` | boolean | Whether more notes are available | +| `cursor` | string | Pagination cursor for the next page | + +### `granola_get_note` + +Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Granola API key | +| `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) | +| `includeTranscript` | boolean | No | Whether to include the meeting transcript | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Note ID | +| `title` | string | Note title | +| `ownerName` | string | Note owner name | +| `ownerEmail` | string | Note owner email | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `summaryText` | string | Plain text summary of the meeting | +| `summaryMarkdown` | string | Markdown-formatted summary of the meeting | +| `attendees` | json | Meeting attendees | +| ↳ `name` | string | Attendee name | +| ↳ `email` | string | Attendee email | +| `folders` | json | Folders the note belongs to | +| ↳ `id` | string | Folder ID | +| ↳ `name` | string | Folder name | +| `calendarEventTitle` | string | Calendar event title | +| `calendarOrganiser` | string | Calendar event organiser email | +| `calendarEventId` | string | Calendar event ID | +| `scheduledStartTime` | string | Scheduled start time | +| `scheduledEndTime` | string | Scheduled end time | +| `invitees` | json | Calendar event invitee emails | +| `transcript` | json | Meeting transcript entries \(only if requested\) | +| ↳ `speaker` | string | Speaker source \(microphone or speaker\) | +| ↳ `text` | string | Transcript text | +| ↳ `startTime` | string | Segment start time | +| ↳ `endTime` | string | Segment end time | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9ff8f3ff78a..549aa09bb7c 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -68,6 +68,7 @@ "google_vault", "grafana", "grain", + "granola", "greenhouse", "greptile", "hex", diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 7580b713c57..c2e2c8f975f 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -74,6 +74,7 @@ import { GoogleVaultIcon, GrafanaIcon, GrainIcon, + GranolaIcon, GreenhouseIcon, GreptileIcon, HexIcon, @@ -247,6 +248,7 @@ export const blockTypeToIconMap: Record = { google_vault: GoogleVaultIcon, grafana: GrafanaIcon, grain: GrainIcon, + granola: GranolaIcon, greenhouse: GreenhouseIcon, greptile: GreptileIcon, hex: HexIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 7d8cf706be5..67ab2d0bca8 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -4871,6 +4871,33 @@ "integrationType": "media", "tags": ["meeting", "note-taking"] }, + { + "type": "granola", + "slug": "granola", + "name": "Granola", + "description": "Access meeting notes and transcripts from Granola", + "longDescription": "Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts.", + "bgColor": "#B2C147", + "iconName": "GranolaIcon", + "docsUrl": "https://docs.sim.ai/tools/granola", + "operations": [ + { + "name": "List Notes", + "description": "Lists meeting notes from Granola with optional date filters and pagination." + }, + { + "name": "Get Note", + "description": "Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript." + } + ], + "operationCount": 2, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "productivity", + "tags": ["meeting", "note-taking"] + }, { "type": "greenhouse", "slug": "greenhouse", diff --git a/apps/sim/blocks/blocks/granola.ts b/apps/sim/blocks/blocks/granola.ts new file mode 100644 index 00000000000..b919df3f0dc --- /dev/null +++ b/apps/sim/blocks/blocks/granola.ts @@ -0,0 +1,163 @@ +import { GranolaIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' + +export const GranolaBlock: BlockConfig = { + type: 'granola', + name: 'Granola', + description: 'Access meeting notes and transcripts from Granola', + longDescription: + 'Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts.', + docsLink: 'https://docs.sim.ai/tools/granola', + category: 'tools', + integrationType: IntegrationType.Productivity, + tags: ['meeting', 'note-taking'], + bgColor: '#B2C147', + icon: GranolaIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Notes', id: 'list_notes' }, + { label: 'Get Note', id: 'get_note' }, + ], + value: () => 'list_notes', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Granola API key', + password: true, + }, + { + id: 'noteId', + title: 'Note ID', + type: 'short-input', + required: { field: 'operation', value: 'get_note' }, + placeholder: 'e.g., not_1d3tmYTlCICgjy', + condition: { field: 'operation', value: 'get_note' }, + }, + { + id: 'includeTranscript', + title: 'Include Transcript', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'get_note' }, + mode: 'advanced', + }, + { + id: 'createdAfter', + title: 'Created After', + type: 'short-input', + placeholder: 'e.g., 2026-01-01', + condition: { field: 'operation', value: 'list_notes' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 date or datetime string. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'createdBefore', + title: 'Created Before', + type: 'short-input', + placeholder: 'e.g., 2026-03-01', + condition: { field: 'operation', value: 'list_notes' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 date or datetime string. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'updatedAfter', + title: 'Updated After', + type: 'short-input', + placeholder: 'e.g., 2026-01-01', + condition: { field: 'operation', value: 'list_notes' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 date or datetime string. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '10 (1-30)', + condition: { field: 'operation', value: 'list_notes' }, + mode: 'advanced', + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from previous response', + condition: { field: 'operation', value: 'list_notes' }, + mode: 'advanced', + }, + ], + + tools: { + access: ['granola_list_notes', 'granola_get_note'], + config: { + tool: (params) => `granola_${params.operation}`, + params: (params) => { + const result: Record = {} + if (params.includeTranscript === 'true') result.includeTranscript = true + if (params.pageSize) result.pageSize = Number(params.pageSize) + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Granola API key' }, + noteId: { type: 'string', description: 'Note ID for get_note operation' }, + includeTranscript: { type: 'string', description: 'Whether to include transcript' }, + createdAfter: { type: 'string', description: 'Filter notes created after this date' }, + createdBefore: { type: 'string', description: 'Filter notes created before this date' }, + updatedAfter: { type: 'string', description: 'Filter notes updated after this date' }, + pageSize: { type: 'number', description: 'Results per page (1-30)' }, + cursor: { type: 'string', description: 'Pagination cursor' }, + }, + + outputs: { + notes: { + type: 'json', + description: 'List of meeting notes (id, title, ownerName, ownerEmail, createdAt, updatedAt)', + }, + hasMore: { type: 'boolean', description: 'Whether more notes are available' }, + cursor: { type: 'string', description: 'Pagination cursor for next page' }, + id: { type: 'string', description: 'Note ID' }, + title: { type: 'string', description: 'Note title' }, + summaryText: { type: 'string', description: 'Plain text meeting summary' }, + summaryMarkdown: { type: 'string', description: 'Markdown meeting summary' }, + attendees: { type: 'json', description: 'Meeting attendees (name, email)' }, + folders: { type: 'json', description: 'Folders the note belongs to (id, name)' }, + calendarEventTitle: { type: 'string', description: 'Calendar event title' }, + calendarOrganiser: { type: 'string', description: 'Calendar event organiser email' }, + invitees: { type: 'json', description: 'Calendar event invitee emails' }, + transcript: { + type: 'json', + description: 'Meeting transcript entries (speaker, text, startTime, endTime)', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 15363a8ad53..697b18af433 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -73,6 +73,7 @@ import { GoogleTranslateBlock } from '@/blocks/blocks/google_translate' import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' import { GrafanaBlock } from '@/blocks/blocks/grafana' import { GrainBlock } from '@/blocks/blocks/grain' +import { GranolaBlock } from '@/blocks/blocks/granola' import { GreenhouseBlock } from '@/blocks/blocks/greenhouse' import { GreptileBlock } from '@/blocks/blocks/greptile' import { GuardrailsBlock } from '@/blocks/blocks/guardrails' @@ -292,6 +293,7 @@ export const registry: Record = { google_vault: GoogleVaultBlock, grafana: GrafanaBlock, grain: GrainBlock, + granola: GranolaBlock, greenhouse: GreenhouseBlock, greptile: GreptileBlock, guardrails: GuardrailsBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 0cf5773ca48..cd0c085f913 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -5091,6 +5091,17 @@ export function GrainIcon(props: SVGProps) { ) } +export function GranolaIcon(props: SVGProps) { + return ( + + + + ) +} + export function CirclebackIcon(props: SVGProps) { const id = useId() const patternId = `circleback_pattern_${id}` diff --git a/apps/sim/tools/granola/get_note.ts b/apps/sim/tools/granola/get_note.ts new file mode 100644 index 00000000000..c72749c9890 --- /dev/null +++ b/apps/sim/tools/granola/get_note.ts @@ -0,0 +1,156 @@ +import type { GranolaGetNoteParams, GranolaGetNoteResponse } from '@/tools/granola/types' +import type { ToolConfig } from '@/tools/types' + +export const getNoteTool: ToolConfig = { + id: 'granola_get_note', + name: 'Granola Get Note', + description: + 'Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Granola API key', + }, + noteId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The note ID (e.g., not_1d3tmYTlCICgjy)', + }, + includeTranscript: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include the meeting transcript', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://public-api.granola.ai/v1/notes/${params.noteId.trim()}`) + if (params.includeTranscript) url.searchParams.append('include', 'transcript') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.text() + throw new Error(`Granola API error (${response.status}): ${error}`) + } + + const data = await response.json() + + return { + success: true, + output: { + id: data.id ?? '', + title: data.title ?? null, + ownerName: data.owner?.name ?? null, + ownerEmail: data.owner?.email ?? '', + createdAt: data.created_at ?? '', + updatedAt: data.updated_at ?? '', + summaryText: data.summary_text ?? '', + summaryMarkdown: data.summary_markdown ?? null, + attendees: (data.attendees ?? []).map((a: { name: string | null; email: string }) => ({ + name: a.name ?? null, + email: a.email ?? '', + })), + folders: (data.folder_membership ?? []).map((f: { id: string; name: string }) => ({ + id: f.id ?? '', + name: f.name ?? '', + })), + calendarEventTitle: data.calendar_event?.event_title ?? null, + calendarOrganiser: data.calendar_event?.organiser ?? null, + calendarEventId: data.calendar_event?.calendar_event_id ?? null, + scheduledStartTime: data.calendar_event?.scheduled_start_time ?? null, + scheduledEndTime: data.calendar_event?.scheduled_end_time ?? null, + invitees: (data.calendar_event?.invitees ?? []).map((i: { email: string }) => i.email), + transcript: data.transcript + ? data.transcript.map( + (t: { + speaker: { source: string } + text: string + start_time: string + end_time: string + }) => ({ + speaker: t.speaker?.source ?? 'unknown', + text: t.text ?? '', + startTime: t.start_time ?? '', + endTime: t.end_time ?? '', + }) + ) + : null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Note ID' }, + title: { type: 'string', description: 'Note title', optional: true }, + ownerName: { type: 'string', description: 'Note owner name', optional: true }, + ownerEmail: { type: 'string', description: 'Note owner email' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + summaryText: { type: 'string', description: 'Plain text summary of the meeting' }, + summaryMarkdown: { + type: 'string', + description: 'Markdown-formatted summary of the meeting', + optional: true, + }, + attendees: { + type: 'json', + description: 'Meeting attendees', + properties: { + name: { type: 'string', description: 'Attendee name' }, + email: { type: 'string', description: 'Attendee email' }, + }, + }, + folders: { + type: 'json', + description: 'Folders the note belongs to', + properties: { + id: { type: 'string', description: 'Folder ID' }, + name: { type: 'string', description: 'Folder name' }, + }, + }, + calendarEventTitle: { + type: 'string', + description: 'Calendar event title', + optional: true, + }, + calendarOrganiser: { + type: 'string', + description: 'Calendar event organiser email', + optional: true, + }, + calendarEventId: { type: 'string', description: 'Calendar event ID', optional: true }, + scheduledStartTime: { + type: 'string', + description: 'Scheduled start time', + optional: true, + }, + scheduledEndTime: { type: 'string', description: 'Scheduled end time', optional: true }, + invitees: { type: 'json', description: 'Calendar event invitee emails' }, + transcript: { + type: 'json', + description: 'Meeting transcript entries (only if requested)', + optional: true, + properties: { + speaker: { type: 'string', description: 'Speaker source (microphone or speaker)' }, + text: { type: 'string', description: 'Transcript text' }, + startTime: { type: 'string', description: 'Segment start time' }, + endTime: { type: 'string', description: 'Segment end time' }, + }, + }, + }, +} diff --git a/apps/sim/tools/granola/index.ts b/apps/sim/tools/granola/index.ts new file mode 100644 index 00000000000..27d011ad437 --- /dev/null +++ b/apps/sim/tools/granola/index.ts @@ -0,0 +1,5 @@ +import { getNoteTool } from '@/tools/granola/get_note' +import { listNotesTool } from '@/tools/granola/list_notes' + +export const granolaListNotesTool = listNotesTool +export const granolaGetNoteTool = getNoteTool diff --git a/apps/sim/tools/granola/list_notes.ts b/apps/sim/tools/granola/list_notes.ts new file mode 100644 index 00000000000..032b8eef6d1 --- /dev/null +++ b/apps/sim/tools/granola/list_notes.ts @@ -0,0 +1,122 @@ +import type { GranolaListNotesParams, GranolaListNotesResponse } from '@/tools/granola/types' +import type { ToolConfig } from '@/tools/types' + +export const listNotesTool: ToolConfig = { + id: 'granola_list_notes', + name: 'Granola List Notes', + description: 'Lists meeting notes from Granola with optional date filters and pagination.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Granola API key', + }, + createdBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Return notes created before this date (ISO 8601)', + }, + createdAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Return notes created after this date (ISO 8601)', + }, + updatedAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Return notes updated after this date (ISO 8601)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of notes per page (1-30, default 10)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://public-api.granola.ai/v1/notes') + if (params.createdBefore) url.searchParams.append('created_before', params.createdBefore) + if (params.createdAfter) url.searchParams.append('created_after', params.createdAfter) + if (params.updatedAfter) url.searchParams.append('updated_after', params.updatedAfter) + if (params.cursor) url.searchParams.append('cursor', params.cursor) + if (params.pageSize) url.searchParams.append('page_size', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.text() + throw new Error(`Granola API error (${response.status}): ${error}`) + } + + const data = await response.json() + + return { + success: true, + output: { + notes: (data.notes ?? []).map( + (note: { + id: string + title: string | null + owner: { name: string | null; email: string } + created_at: string + updated_at: string + }) => ({ + id: note.id, + title: note.title ?? null, + ownerName: note.owner?.name ?? null, + ownerEmail: note.owner?.email ?? '', + createdAt: note.created_at ?? '', + updatedAt: note.updated_at ?? '', + }) + ), + hasMore: data.hasMore ?? false, + cursor: data.cursor ?? null, + }, + } + }, + + outputs: { + notes: { + type: 'json', + description: 'List of meeting notes', + properties: { + id: { type: 'string', description: 'Note ID' }, + title: { type: 'string', description: 'Note title' }, + ownerName: { type: 'string', description: 'Note owner name' }, + ownerEmail: { type: 'string', description: 'Note owner email' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + hasMore: { + type: 'boolean', + description: 'Whether more notes are available', + }, + cursor: { + type: 'string', + description: 'Pagination cursor for the next page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/granola/types.ts b/apps/sim/tools/granola/types.ts new file mode 100644 index 00000000000..eed9a2f32a2 --- /dev/null +++ b/apps/sim/tools/granola/types.ts @@ -0,0 +1,53 @@ +import type { ToolResponse } from '@/tools/types' + +export interface GranolaListNotesParams { + apiKey: string + createdBefore?: string + createdAfter?: string + updatedAfter?: string + cursor?: string + pageSize?: number +} + +export interface GranolaGetNoteParams { + apiKey: string + noteId: string + includeTranscript?: boolean +} + +export interface GranolaListNotesResponse extends ToolResponse { + output: { + notes: { + id: string + title: string | null + ownerName: string | null + ownerEmail: string + createdAt: string + updatedAt: string + }[] + hasMore: boolean + cursor: string | null + } +} + +export interface GranolaGetNoteResponse extends ToolResponse { + output: { + id: string + title: string | null + ownerName: string | null + ownerEmail: string + createdAt: string + updatedAt: string + summaryText: string + summaryMarkdown: string | null + attendees: { name: string | null; email: string }[] + folders: { id: string; name: string }[] + calendarEventTitle: string | null + calendarOrganiser: string | null + calendarEventId: string | null + scheduledStartTime: string | null + scheduledEndTime: string | null + invitees: string[] + transcript: { speaker: string; text: string; startTime: string; endTime: string }[] | null + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 99efb6dcbbc..c754d0e80db 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -980,6 +980,7 @@ import { grainListTeamsTool, grainListViewsTool, } from '@/tools/grain' +import { granolaGetNoteTool, granolaListNotesTool } from '@/tools/granola' import { greenhouseGetApplicationTool, greenhouseGetCandidateTool, @@ -2660,6 +2661,8 @@ export const tools: Record = { greenhouse_list_departments: greenhouseListDepartmentsTool, greenhouse_list_offices: greenhouseListOfficesTool, greenhouse_list_job_stages: greenhouseListJobStagesTool, + granola_list_notes: granolaListNotesTool, + granola_get_note: granolaGetNoteTool, guardrails_validate: guardrailsValidateTool, hex_cancel_run: hexCancelRunTool, hex_create_collection: hexCreateCollectionTool, From 3ce86b8dadcad5ad58eb8d7887a1564937fe1e59 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 13:32:50 -0700 Subject: [PATCH 2/4] fix(granola): use string comparison for includeTranscript to avoid truthy string bug --- apps/sim/blocks/blocks/granola.ts | 1 - apps/sim/tools/granola/get_note.ts | 4 ++-- apps/sim/tools/granola/types.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/sim/blocks/blocks/granola.ts b/apps/sim/blocks/blocks/granola.ts index b919df3f0dc..578bbacae52 100644 --- a/apps/sim/blocks/blocks/granola.ts +++ b/apps/sim/blocks/blocks/granola.ts @@ -120,7 +120,6 @@ export const GranolaBlock: BlockConfig = { tool: (params) => `granola_${params.operation}`, params: (params) => { const result: Record = {} - if (params.includeTranscript === 'true') result.includeTranscript = true if (params.pageSize) result.pageSize = Number(params.pageSize) return result }, diff --git a/apps/sim/tools/granola/get_note.ts b/apps/sim/tools/granola/get_note.ts index c72749c9890..a4b1556d7eb 100644 --- a/apps/sim/tools/granola/get_note.ts +++ b/apps/sim/tools/granola/get_note.ts @@ -22,7 +22,7 @@ export const getNoteTool: ToolConfig { const url = new URL(`https://public-api.granola.ai/v1/notes/${params.noteId.trim()}`) - if (params.includeTranscript) url.searchParams.append('include', 'transcript') + if (params.includeTranscript === 'true') url.searchParams.append('include', 'transcript') return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/granola/types.ts b/apps/sim/tools/granola/types.ts index eed9a2f32a2..b9328262cbc 100644 --- a/apps/sim/tools/granola/types.ts +++ b/apps/sim/tools/granola/types.ts @@ -12,7 +12,7 @@ export interface GranolaListNotesParams { export interface GranolaGetNoteParams { apiKey: string noteId: string - includeTranscript?: boolean + includeTranscript?: string } export interface GranolaListNotesResponse extends ToolResponse { From 24515fab5bd54ae943c85758fe2dc1afd2f9953a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 14:57:19 -0700 Subject: [PATCH 3/4] fix(granola): add missing get_note output fields to block definition --- apps/sim/blocks/blocks/granola.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/sim/blocks/blocks/granola.ts b/apps/sim/blocks/blocks/granola.ts index 578bbacae52..0062fbee815 100644 --- a/apps/sim/blocks/blocks/granola.ts +++ b/apps/sim/blocks/blocks/granola.ts @@ -147,12 +147,19 @@ export const GranolaBlock: BlockConfig = { cursor: { type: 'string', description: 'Pagination cursor for next page' }, id: { type: 'string', description: 'Note ID' }, title: { type: 'string', description: 'Note title' }, + ownerName: { type: 'string', description: 'Note owner name' }, + ownerEmail: { type: 'string', description: 'Note owner email' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, summaryText: { type: 'string', description: 'Plain text meeting summary' }, summaryMarkdown: { type: 'string', description: 'Markdown meeting summary' }, attendees: { type: 'json', description: 'Meeting attendees (name, email)' }, folders: { type: 'json', description: 'Folders the note belongs to (id, name)' }, calendarEventTitle: { type: 'string', description: 'Calendar event title' }, calendarOrganiser: { type: 'string', description: 'Calendar event organiser email' }, + calendarEventId: { type: 'string', description: 'Calendar event ID' }, + scheduledStartTime: { type: 'string', description: 'Scheduled start time' }, + scheduledEndTime: { type: 'string', description: 'Scheduled end time' }, invitees: { type: 'json', description: 'Calendar event invitee emails' }, transcript: { type: 'json', From 3c76457cc32720ac3b3006db2c3278ee754bb6bf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 14:59:49 -0700 Subject: [PATCH 4/4] regen docs --- apps/docs/content/docs/en/tools/granola.mdx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/apps/docs/content/docs/en/tools/granola.mdx b/apps/docs/content/docs/en/tools/granola.mdx index 9bdefed216d..88eb50622a3 100644 --- a/apps/docs/content/docs/en/tools/granola.mdx +++ b/apps/docs/content/docs/en/tools/granola.mdx @@ -10,23 +10,12 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#B2C147" /> -{/* MANUAL-CONTENT-START:intro */} -[Granola](https://www.granola.ai/) is an AI-powered meeting notes platform that automatically captures, transcribes, and summarizes your meetings. Granola runs quietly in the background during calls, producing structured notes with key takeaways so you never miss important details. - -With the Granola integration in Sim, you can: - -- **List meeting notes**: Retrieve your recent meeting notes with flexible date and pagination filters -- **Get note details**: Access the full summary, attendees, calendar event metadata, and folder membership for any note -- **Include transcripts**: Optionally retrieve the complete speaker-attributed transcript for a meeting -- **Filter by date**: Narrow results to notes created before/after a date or updated after a date - -In Sim, the Granola integration enables your agents to programmatically access meeting intelligence as part of their workflows. This allows for automation scenarios such as extracting action items from meetings, syncing meeting summaries to other tools, building meeting digest reports, or triggering follow-up tasks based on meeting content. -{/* MANUAL-CONTENT-END:intro */} - ## Usage Instructions Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts. + + ## Tools ### `granola_list_notes` @@ -68,7 +57,7 @@ Retrieves a specific meeting note from Granola by ID, including summary, attende | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Granola API key | | `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) | -| `includeTranscript` | boolean | No | Whether to include the meeting transcript | +| `includeTranscript` | string | No | Whether to include the meeting transcript | #### Output