diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 9db82b6d349..2655c7fdf4e 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -8138,8 +8138,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 82fae4c2c3f..34f5198aeeb 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 00000000000..8155fc67084 --- /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 00ae58a21b1..3320176b89d 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -20,6 +20,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 { rssHandler } from '@/lib/webhooks/providers/rss' import { slackHandler } from '@/lib/webhooks/providers/slack' @@ -56,6 +57,7 @@ const PROVIDER_HANDLERS: Record = { lemlist: lemlistHandler, linear: linearHandler, '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 00000000000..afa154b2d22 --- /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 00000000000..62d9d8cb133 --- /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 00000000000..0bd05796adb --- /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 00000000000..85b3511f43b --- /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 00000000000..b21334103a3 --- /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 00000000000..1fb7134ad4d --- /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 00000000000..da176d4ff53 --- /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 00000000000..641fa0f3cb2 --- /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 00000000000..76a578eec59 --- /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 00000000000..df8d7a4b5b2 --- /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 00000000000..db3a824df0d --- /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 3c1845a7fd0..3197869920b 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -170,6 +170,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 { rssPollingTrigger } from '@/triggers/rss' import { @@ -314,6 +325,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, rss_poller: rssPollingTrigger, salesforce_record_created: salesforceRecordCreatedTrigger,