From 21e5b5c594c2c96bb03ec09944dc2e7ffaaa6715 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 6 Apr 2026 12:05:00 -0700 Subject: [PATCH] feat(triggers): add Notion webhook triggers (#3989) * feat(triggers): add Notion webhook triggers for all event types Add 9 Notion webhook triggers covering the full event lifecycle: - Page events: created, properties updated, content updated, deleted - Database events: created, schema updated, deleted - Comment events: created - Generic webhook trigger (all events) Implements provider handler with HMAC SHA-256 signature verification, event filtering via matchEvent, and structured input formatting. Co-Authored-By: Claude Opus 4.6 * fix(triggers): resolve type field collision in Notion trigger outputs Rename nested `type` fields to `entity_type`/`parent_type` to avoid collision with processOutputField's leaf node detection which checks `'type' in field`. Remove spread of author outputs into `authors` array which was overwriting `type: 'array'`. Co-Authored-By: Claude Opus 4.6 * fix(triggers): clarify Notion webhook signing secret vs verification_token Update placeholder and description to distinguish the signing secret (used for HMAC-SHA256 signature verification) from the verification_token (one-time challenge echoed during initial setup). Co-Authored-By: Claude Opus 4.6 * refactor(webhooks): use createHmacVerifier for Notion provider handler Replace inline verifyAuth boilerplate with createHmacVerifier utility, consistent with Linear, Ashby, Cal.com, Circleback, Confluence, and Fireflies providers. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../integrations/data/integrations.json | 50 ++++- apps/sim/blocks/blocks/notion.ts | 30 ++- apps/sim/lib/webhooks/providers/notion.ts | 100 +++++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/triggers/notion/comment_created.ts | 38 ++++ apps/sim/triggers/notion/database_created.ts | 38 ++++ apps/sim/triggers/notion/database_deleted.ts | 38 ++++ .../notion/database_schema_updated.ts | 40 ++++ apps/sim/triggers/notion/index.ts | 9 + .../triggers/notion/page_content_updated.ts | 40 ++++ apps/sim/triggers/notion/page_created.ts | 41 ++++ apps/sim/triggers/notion/page_deleted.ts | 38 ++++ .../notion/page_properties_updated.ts | 40 ++++ apps/sim/triggers/notion/utils.ts | 201 ++++++++++++++++++ apps/sim/triggers/notion/webhook.ts | 38 ++++ apps/sim/triggers/registry.ts | 20 ++ 16 files changed, 760 insertions(+), 3 deletions(-) create mode 100644 apps/sim/lib/webhooks/providers/notion.ts create mode 100644 apps/sim/triggers/notion/comment_created.ts create mode 100644 apps/sim/triggers/notion/database_created.ts create mode 100644 apps/sim/triggers/notion/database_deleted.ts create mode 100644 apps/sim/triggers/notion/database_schema_updated.ts create mode 100644 apps/sim/triggers/notion/index.ts create mode 100644 apps/sim/triggers/notion/page_content_updated.ts create mode 100644 apps/sim/triggers/notion/page_created.ts create mode 100644 apps/sim/triggers/notion/page_deleted.ts create mode 100644 apps/sim/triggers/notion/page_properties_updated.ts create mode 100644 apps/sim/triggers/notion/utils.ts create mode 100644 apps/sim/triggers/notion/webhook.ts diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 189e0a3b24..cb5d3cbdb4 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -8180,8 +8180,54 @@ "docsUrl": "https://docs.sim.ai/tools/notion", "operations": [], "operationCount": 0, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "notion_page_created", + "name": "Notion Page Created", + "description": "Trigger workflow when a new page is created in Notion" + }, + { + "id": "notion_page_properties_updated", + "name": "Notion Page Properties Updated", + "description": "Trigger workflow when page properties are modified in Notion" + }, + { + "id": "notion_page_content_updated", + "name": "Notion Page Content Updated", + "description": "Trigger workflow when page content is changed in Notion" + }, + { + "id": "notion_page_deleted", + "name": "Notion Page Deleted", + "description": "Trigger workflow when a page is deleted in Notion" + }, + { + "id": "notion_database_created", + "name": "Notion Database Created", + "description": "Trigger workflow when a new database is created in Notion" + }, + { + "id": "notion_database_schema_updated", + "name": "Notion Database Schema Updated", + "description": "Trigger workflow when a database schema is modified in Notion" + }, + { + "id": "notion_database_deleted", + "name": "Notion Database Deleted", + "description": "Trigger workflow when a database is deleted in Notion" + }, + { + "id": "notion_comment_created", + "name": "Notion Comment Created", + "description": "Trigger workflow when a comment or suggested edit is added in Notion" + }, + { + "id": "notion_webhook", + "name": "Notion Webhook (All Events)", + "description": "Trigger workflow on any Notion webhook event" + } + ], + "triggerCount": 9, "authType": "oauth", "category": "tools", "integrationType": "documents", diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 82fae4c2c3..34f5198aee 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -3,6 +3,7 @@ import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { createVersionedToolSelector } from '@/blocks/utils' import type { NotionResponse } from '@/tools/notion/types' +import { getTrigger } from '@/triggers' // Legacy block - hidden from toolbar export const NotionBlock: BlockConfig = { @@ -436,7 +437,34 @@ export const NotionV2Block: BlockConfig = { bgColor: '#181C1E', icon: NotionIcon, hideFromToolbar: false, - subBlocks: NotionBlock.subBlocks, + subBlocks: [ + ...NotionBlock.subBlocks, + + // Trigger subBlocks + ...getTrigger('notion_page_created').subBlocks, + ...getTrigger('notion_page_properties_updated').subBlocks, + ...getTrigger('notion_page_content_updated').subBlocks, + ...getTrigger('notion_page_deleted').subBlocks, + ...getTrigger('notion_database_created').subBlocks, + ...getTrigger('notion_database_schema_updated').subBlocks, + ...getTrigger('notion_database_deleted').subBlocks, + ...getTrigger('notion_comment_created').subBlocks, + ...getTrigger('notion_webhook').subBlocks, + ], + triggers: { + enabled: true, + available: [ + 'notion_page_created', + 'notion_page_properties_updated', + 'notion_page_content_updated', + 'notion_page_deleted', + 'notion_database_created', + 'notion_database_schema_updated', + 'notion_database_deleted', + 'notion_comment_created', + 'notion_webhook', + ], + }, tools: { access: [ 'notion_read_v2', diff --git a/apps/sim/lib/webhooks/providers/notion.ts b/apps/sim/lib/webhooks/providers/notion.ts new file mode 100644 index 0000000000..8155fc6708 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/notion.ts @@ -0,0 +1,100 @@ +import crypto from 'crypto' +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { safeCompare } from '@/lib/core/security/encryption' +import type { + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { createHmacVerifier } from '@/lib/webhooks/providers/utils' + +const logger = createLogger('WebhookProvider:Notion') + +/** + * Validates a Notion webhook signature using HMAC SHA-256. + * Notion sends X-Notion-Signature as "sha256=". + */ +function validateNotionSignature(secret: string, signature: string, body: string): boolean { + try { + if (!secret || !signature || !body) { + logger.warn('Notion signature validation missing required fields', { + hasSecret: !!secret, + hasSignature: !!signature, + hasBody: !!body, + }) + return false + } + + const providedHash = signature.startsWith('sha256=') ? signature.slice(7) : signature + const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex') + + logger.debug('Notion signature comparison', { + computedSignature: `${computedHash.substring(0, 10)}...`, + providedSignature: `${providedHash.substring(0, 10)}...`, + computedLength: computedHash.length, + providedLength: providedHash.length, + match: computedHash === providedHash, + }) + + return safeCompare(computedHash, providedHash) + } catch (error) { + logger.error('Error validating Notion signature:', error) + return false + } +} + +export const notionHandler: WebhookProviderHandler = { + verifyAuth: createHmacVerifier({ + configKey: 'webhookSecret', + headerName: 'X-Notion-Signature', + validateFn: validateNotionSignature, + providerLabel: 'Notion', + }), + + async formatInput({ body }: FormatInputContext): Promise { + const b = body as Record + return { + input: { + id: b.id, + type: b.type, + timestamp: b.timestamp, + workspace_id: b.workspace_id, + workspace_name: b.workspace_name, + subscription_id: b.subscription_id, + integration_id: b.integration_id, + attempt_number: b.attempt_number, + authors: b.authors || [], + entity: b.entity || {}, + data: b.data || {}, + }, + } + }, + + async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { + const triggerId = providerConfig.triggerId as string | undefined + const obj = body as Record + + if (triggerId && triggerId !== 'notion_webhook') { + const { isNotionPayloadMatch } = await import('@/triggers/notion/utils') + if (!isNotionPayloadMatch(triggerId, obj)) { + const eventType = obj.type as string | undefined + logger.debug( + `[${requestId}] Notion event mismatch for trigger ${triggerId}. Event: ${eventType}. Skipping execution.`, + { + webhookId: webhook.id, + workflowId: workflow.id, + triggerId, + receivedEvent: eventType, + } + ) + return NextResponse.json({ + message: 'Event type does not match trigger configuration. Ignoring.', + }) + } + } + + return true + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 4c34129720..6b7d6d3d7e 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -23,6 +23,7 @@ import { jiraHandler } from '@/lib/webhooks/providers/jira' import { lemlistHandler } from '@/lib/webhooks/providers/lemlist' import { linearHandler } from '@/lib/webhooks/providers/linear' import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams' +import { notionHandler } from '@/lib/webhooks/providers/notion' import { outlookHandler } from '@/lib/webhooks/providers/outlook' import { resendHandler } from '@/lib/webhooks/providers/resend' import { rssHandler } from '@/lib/webhooks/providers/rss' @@ -64,6 +65,7 @@ const PROVIDER_HANDLERS: Record = { linear: linearHandler, resend: resendHandler, 'microsoft-teams': microsoftTeamsHandler, + notion: notionHandler, outlook: outlookHandler, rss: rssHandler, slack: slackHandler, diff --git a/apps/sim/triggers/notion/comment_created.ts b/apps/sim/triggers/notion/comment_created.ts new file mode 100644 index 0000000000..afa154b2d2 --- /dev/null +++ b/apps/sim/triggers/notion/comment_created.ts @@ -0,0 +1,38 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildCommentEventOutputs, + buildNotionExtraFields, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Comment Created Trigger + */ +export const notionCommentCreatedTrigger: TriggerConfig = { + id: 'notion_comment_created', + name: 'Notion Comment Created', + provider: 'notion', + description: 'Trigger workflow when a comment or suggested edit is added in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_comment_created', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('comment.created'), + extraFields: buildNotionExtraFields('notion_comment_created'), + }), + + outputs: buildCommentEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/database_created.ts b/apps/sim/triggers/notion/database_created.ts new file mode 100644 index 0000000000..62d9d8cb13 --- /dev/null +++ b/apps/sim/triggers/notion/database_created.ts @@ -0,0 +1,38 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildDatabaseEventOutputs, + buildNotionExtraFields, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Database Created Trigger + */ +export const notionDatabaseCreatedTrigger: TriggerConfig = { + id: 'notion_database_created', + name: 'Notion Database Created', + provider: 'notion', + description: 'Trigger workflow when a new database is created in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_database_created', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('database.created'), + extraFields: buildNotionExtraFields('notion_database_created'), + }), + + outputs: buildDatabaseEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/database_deleted.ts b/apps/sim/triggers/notion/database_deleted.ts new file mode 100644 index 0000000000..0bd05796ad --- /dev/null +++ b/apps/sim/triggers/notion/database_deleted.ts @@ -0,0 +1,38 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildDatabaseEventOutputs, + buildNotionExtraFields, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Database Deleted Trigger + */ +export const notionDatabaseDeletedTrigger: TriggerConfig = { + id: 'notion_database_deleted', + name: 'Notion Database Deleted', + provider: 'notion', + description: 'Trigger workflow when a database is deleted in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_database_deleted', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('database.deleted'), + extraFields: buildNotionExtraFields('notion_database_deleted'), + }), + + outputs: buildDatabaseEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/database_schema_updated.ts b/apps/sim/triggers/notion/database_schema_updated.ts new file mode 100644 index 0000000000..85b3511f43 --- /dev/null +++ b/apps/sim/triggers/notion/database_schema_updated.ts @@ -0,0 +1,40 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildDatabaseEventOutputs, + buildNotionExtraFields, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Database Schema Updated Trigger + * + * Fires when a database schema (properties/columns) is modified. + */ +export const notionDatabaseSchemaUpdatedTrigger: TriggerConfig = { + id: 'notion_database_schema_updated', + name: 'Notion Database Schema Updated', + provider: 'notion', + description: 'Trigger workflow when a database schema is modified in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_database_schema_updated', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('database.schema_updated'), + extraFields: buildNotionExtraFields('notion_database_schema_updated'), + }), + + outputs: buildDatabaseEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/index.ts b/apps/sim/triggers/notion/index.ts new file mode 100644 index 0000000000..b21334103a --- /dev/null +++ b/apps/sim/triggers/notion/index.ts @@ -0,0 +1,9 @@ +export { notionCommentCreatedTrigger } from './comment_created' +export { notionDatabaseCreatedTrigger } from './database_created' +export { notionDatabaseDeletedTrigger } from './database_deleted' +export { notionDatabaseSchemaUpdatedTrigger } from './database_schema_updated' +export { notionPageContentUpdatedTrigger } from './page_content_updated' +export { notionPageCreatedTrigger } from './page_created' +export { notionPageDeletedTrigger } from './page_deleted' +export { notionPagePropertiesUpdatedTrigger } from './page_properties_updated' +export { notionWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/notion/page_content_updated.ts b/apps/sim/triggers/notion/page_content_updated.ts new file mode 100644 index 0000000000..1fb7134ad4 --- /dev/null +++ b/apps/sim/triggers/notion/page_content_updated.ts @@ -0,0 +1,40 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNotionExtraFields, + buildPageEventOutputs, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Page Content Updated Trigger + * + * Fires when page content changes. High-frequency events may be batched. + */ +export const notionPageContentUpdatedTrigger: TriggerConfig = { + id: 'notion_page_content_updated', + name: 'Notion Page Content Updated', + provider: 'notion', + description: 'Trigger workflow when page content is changed in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_page_content_updated', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('page.content_updated'), + extraFields: buildNotionExtraFields('notion_page_content_updated'), + }), + + outputs: buildPageEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/page_created.ts b/apps/sim/triggers/notion/page_created.ts new file mode 100644 index 0000000000..da176d4ff5 --- /dev/null +++ b/apps/sim/triggers/notion/page_created.ts @@ -0,0 +1,41 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNotionExtraFields, + buildPageEventOutputs, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Page Created Trigger + * + * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type. + */ +export const notionPageCreatedTrigger: TriggerConfig = { + id: 'notion_page_created', + name: 'Notion Page Created', + provider: 'notion', + description: 'Trigger workflow when a new page is created in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_page_created', + triggerOptions: notionTriggerOptions, + includeDropdown: true, + setupInstructions: notionSetupInstructions('page.created'), + extraFields: buildNotionExtraFields('notion_page_created'), + }), + + outputs: buildPageEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/page_deleted.ts b/apps/sim/triggers/notion/page_deleted.ts new file mode 100644 index 0000000000..641fa0f3cb --- /dev/null +++ b/apps/sim/triggers/notion/page_deleted.ts @@ -0,0 +1,38 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNotionExtraFields, + buildPageEventOutputs, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Page Deleted Trigger + */ +export const notionPageDeletedTrigger: TriggerConfig = { + id: 'notion_page_deleted', + name: 'Notion Page Deleted', + provider: 'notion', + description: 'Trigger workflow when a page is deleted in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_page_deleted', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('page.deleted'), + extraFields: buildNotionExtraFields('notion_page_deleted'), + }), + + outputs: buildPageEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/page_properties_updated.ts b/apps/sim/triggers/notion/page_properties_updated.ts new file mode 100644 index 0000000000..76a578eec5 --- /dev/null +++ b/apps/sim/triggers/notion/page_properties_updated.ts @@ -0,0 +1,40 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNotionExtraFields, + buildPageEventOutputs, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Page Properties Updated Trigger + * + * Fires when page properties (title, status, tags, etc.) are modified. + */ +export const notionPagePropertiesUpdatedTrigger: TriggerConfig = { + id: 'notion_page_properties_updated', + name: 'Notion Page Properties Updated', + provider: 'notion', + description: 'Trigger workflow when page properties are modified in Notion', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_page_properties_updated', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('page.properties_updated'), + extraFields: buildNotionExtraFields('notion_page_properties_updated'), + }), + + outputs: buildPageEventOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/notion/utils.ts b/apps/sim/triggers/notion/utils.ts new file mode 100644 index 0000000000..df8d7a4b5b --- /dev/null +++ b/apps/sim/triggers/notion/utils.ts @@ -0,0 +1,201 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Dropdown options for the Notion trigger type selector. + */ +export const notionTriggerOptions = [ + { label: 'Page Created', id: 'notion_page_created' }, + { label: 'Page Properties Updated', id: 'notion_page_properties_updated' }, + { label: 'Page Content Updated', id: 'notion_page_content_updated' }, + { label: 'Page Deleted', id: 'notion_page_deleted' }, + { label: 'Database Created', id: 'notion_database_created' }, + { label: 'Database Schema Updated', id: 'notion_database_schema_updated' }, + { label: 'Database Deleted', id: 'notion_database_deleted' }, + { label: 'Comment Created', id: 'notion_comment_created' }, + { label: 'Generic Webhook (All Events)', id: 'notion_webhook' }, +] + +/** + * Generates HTML setup instructions for Notion webhook triggers. + * Notion webhooks must be configured manually through the integration settings UI. + */ +export function notionSetupInstructions(eventType: string): string { + const instructions = [ + 'Go to notion.so/profile/integrations and select your integration (or create one).', + 'Navigate to the Webhooks tab.', + 'Click "Create a subscription".', + 'Paste the Webhook URL above into the URL field.', + `Select the ${eventType} event type(s).`, + 'Notion will send a verification request. Copy the verification_token from the payload and paste it into the Notion UI to complete verification.', + 'Ensure the integration has access to the pages/databases you want to monitor (share them with the integration).', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Extra fields for Notion triggers (no extra fields needed since setup is manual). + */ +export function buildNotionExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter your Notion webhook signing secret', + description: + 'The signing secret from your Notion integration settings page, used to verify X-Notion-Signature headers. This is separate from the verification_token used during initial setup.', + password: true, + required: false, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Base webhook outputs common to all Notion triggers. + */ +function buildBaseOutputs(): Record { + return { + id: { type: 'string', description: 'Webhook event ID' }, + type: { + type: 'string', + description: 'Event type (e.g., page.created, database.schema_updated)', + }, + timestamp: { type: 'string', description: 'ISO 8601 timestamp of the event' }, + workspace_id: { type: 'string', description: 'Workspace ID where the event occurred' }, + workspace_name: { type: 'string', description: 'Workspace name' }, + subscription_id: { type: 'string', description: 'Webhook subscription ID' }, + integration_id: { type: 'string', description: 'Integration ID that received the event' }, + attempt_number: { type: 'number', description: 'Delivery attempt number' }, + } +} + +/** + * Entity output schema (the resource that was affected). + */ +function buildEntityOutputs(): Record { + return { + id: { type: 'string', description: 'Entity ID (page or database ID)' }, + entity_type: { type: 'string', description: 'Entity type (page or database)' }, + } +} + +/** + * Build outputs for page event triggers. + */ +export function buildPageEventOutputs(): Record { + return { + ...buildBaseOutputs(), + authors: { + type: 'array', + description: 'Array of users who triggered the event', + }, + entity: buildEntityOutputs(), + data: { + parent: { + id: { type: 'string', description: 'Parent page or database ID' }, + parent_type: { type: 'string', description: 'Parent type (database, page, workspace)' }, + }, + }, + } +} + +/** + * Build outputs for database event triggers. + */ +export function buildDatabaseEventOutputs(): Record { + return { + ...buildBaseOutputs(), + authors: { + type: 'array', + description: 'Array of users who triggered the event', + }, + entity: buildEntityOutputs(), + data: { + parent: { + id: { type: 'string', description: 'Parent page or workspace ID' }, + parent_type: { type: 'string', description: 'Parent type (page, workspace)' }, + }, + }, + } +} + +/** + * Build outputs for comment event triggers. + */ +export function buildCommentEventOutputs(): Record { + return { + ...buildBaseOutputs(), + authors: { + type: 'array', + description: 'Array of users who triggered the event', + }, + entity: { + id: { type: 'string', description: 'Comment ID' }, + entity_type: { type: 'string', description: 'Entity type (comment)' }, + }, + data: { + parent: { + id: { type: 'string', description: 'Parent page ID' }, + parent_type: { type: 'string', description: 'Parent type (page)' }, + }, + }, + } +} + +/** + * Build outputs for the generic webhook trigger (all events). + */ +export function buildGenericWebhookOutputs(): Record { + return { + ...buildBaseOutputs(), + authors: { + type: 'array', + description: 'Array of users who triggered the event', + }, + entity: buildEntityOutputs(), + data: { + type: 'json', + description: 'Event-specific data including parent information', + }, + } +} + +/** + * Maps trigger IDs to the Notion event type strings they accept. + */ +const TRIGGER_EVENT_MAP: Record = { + notion_page_created: ['page.created'], + notion_page_properties_updated: ['page.properties_updated'], + notion_page_content_updated: ['page.content_updated'], + notion_page_deleted: ['page.deleted'], + notion_database_created: ['database.created'], + notion_database_schema_updated: ['database.schema_updated'], + notion_database_deleted: ['database.deleted'], + notion_comment_created: ['comment.created'], +} + +/** + * Checks if a Notion webhook payload matches a trigger. + */ +export function isNotionPayloadMatch(triggerId: string, body: Record): boolean { + if (triggerId === 'notion_webhook') { + return true + } + + const eventType = body.type as string | undefined + if (!eventType) { + return false + } + + const acceptedEvents = TRIGGER_EVENT_MAP[triggerId] + return acceptedEvents ? acceptedEvents.includes(eventType) : false +} diff --git a/apps/sim/triggers/notion/webhook.ts b/apps/sim/triggers/notion/webhook.ts new file mode 100644 index 0000000000..db3a824df0 --- /dev/null +++ b/apps/sim/triggers/notion/webhook.ts @@ -0,0 +1,38 @@ +import { NotionIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildGenericWebhookOutputs, + buildNotionExtraFields, + notionSetupInstructions, + notionTriggerOptions, +} from '@/triggers/notion/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Notion Generic Webhook Trigger (All Events) + */ +export const notionWebhookTrigger: TriggerConfig = { + id: 'notion_webhook', + name: 'Notion Webhook (All Events)', + provider: 'notion', + description: 'Trigger workflow on any Notion webhook event', + version: '1.0.0', + icon: NotionIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'notion_webhook', + triggerOptions: notionTriggerOptions, + setupInstructions: notionSetupInstructions('all desired'), + extraFields: buildNotionExtraFields('notion_webhook'), + }), + + outputs: buildGenericWebhookOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Notion-Signature': 'sha256=...', + }, + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index ffd46f277e..abf1cd9706 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -189,6 +189,17 @@ import { microsoftTeamsChatSubscriptionTrigger, microsoftTeamsWebhookTrigger, } from '@/triggers/microsoftteams' +import { + notionCommentCreatedTrigger, + notionDatabaseCreatedTrigger, + notionDatabaseDeletedTrigger, + notionDatabaseSchemaUpdatedTrigger, + notionPageContentUpdatedTrigger, + notionPageCreatedTrigger, + notionPageDeletedTrigger, + notionPagePropertiesUpdatedTrigger, + notionWebhookTrigger, +} from '@/triggers/notion' import { outlookPollingTrigger } from '@/triggers/outlook' import { resendEmailBouncedTrigger, @@ -353,6 +364,15 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { linear_customer_request_updated: linearCustomerRequestUpdatedTrigger, microsoftteams_webhook: microsoftTeamsWebhookTrigger, microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger, + notion_page_created: notionPageCreatedTrigger, + notion_page_properties_updated: notionPagePropertiesUpdatedTrigger, + notion_page_content_updated: notionPageContentUpdatedTrigger, + notion_page_deleted: notionPageDeletedTrigger, + notion_database_created: notionDatabaseCreatedTrigger, + notion_database_schema_updated: notionDatabaseSchemaUpdatedTrigger, + notion_database_deleted: notionDatabaseDeletedTrigger, + notion_comment_created: notionCommentCreatedTrigger, + notion_webhook: notionWebhookTrigger, outlook_poller: outlookPollingTrigger, resend_email_sent: resendEmailSentTrigger, resend_email_delivered: resendEmailDeliveredTrigger,