Skip to content

Commit 21e5b5c

Browse files
waleedlatif1claude
andauthored
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7ea0693 commit 21e5b5c

File tree

16 files changed

+760
-3
lines changed

16 files changed

+760
-3
lines changed

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8180,8 +8180,54 @@
81808180
"docsUrl": "https://docs.sim.ai/tools/notion",
81818181
"operations": [],
81828182
"operationCount": 0,
8183-
"triggers": [],
8184-
"triggerCount": 0,
8183+
"triggers": [
8184+
{
8185+
"id": "notion_page_created",
8186+
"name": "Notion Page Created",
8187+
"description": "Trigger workflow when a new page is created in Notion"
8188+
},
8189+
{
8190+
"id": "notion_page_properties_updated",
8191+
"name": "Notion Page Properties Updated",
8192+
"description": "Trigger workflow when page properties are modified in Notion"
8193+
},
8194+
{
8195+
"id": "notion_page_content_updated",
8196+
"name": "Notion Page Content Updated",
8197+
"description": "Trigger workflow when page content is changed in Notion"
8198+
},
8199+
{
8200+
"id": "notion_page_deleted",
8201+
"name": "Notion Page Deleted",
8202+
"description": "Trigger workflow when a page is deleted in Notion"
8203+
},
8204+
{
8205+
"id": "notion_database_created",
8206+
"name": "Notion Database Created",
8207+
"description": "Trigger workflow when a new database is created in Notion"
8208+
},
8209+
{
8210+
"id": "notion_database_schema_updated",
8211+
"name": "Notion Database Schema Updated",
8212+
"description": "Trigger workflow when a database schema is modified in Notion"
8213+
},
8214+
{
8215+
"id": "notion_database_deleted",
8216+
"name": "Notion Database Deleted",
8217+
"description": "Trigger workflow when a database is deleted in Notion"
8218+
},
8219+
{
8220+
"id": "notion_comment_created",
8221+
"name": "Notion Comment Created",
8222+
"description": "Trigger workflow when a comment or suggested edit is added in Notion"
8223+
},
8224+
{
8225+
"id": "notion_webhook",
8226+
"name": "Notion Webhook (All Events)",
8227+
"description": "Trigger workflow on any Notion webhook event"
8228+
}
8229+
],
8230+
"triggerCount": 9,
81858231
"authType": "oauth",
81868232
"category": "tools",
81878233
"integrationType": "documents",

apps/sim/blocks/blocks/notion.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { BlockConfig } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
44
import { createVersionedToolSelector } from '@/blocks/utils'
55
import type { NotionResponse } from '@/tools/notion/types'
6+
import { getTrigger } from '@/triggers'
67

78
// Legacy block - hidden from toolbar
89
export const NotionBlock: BlockConfig<NotionResponse> = {
@@ -436,7 +437,34 @@ export const NotionV2Block: BlockConfig<any> = {
436437
bgColor: '#181C1E',
437438
icon: NotionIcon,
438439
hideFromToolbar: false,
439-
subBlocks: NotionBlock.subBlocks,
440+
subBlocks: [
441+
...NotionBlock.subBlocks,
442+
443+
// Trigger subBlocks
444+
...getTrigger('notion_page_created').subBlocks,
445+
...getTrigger('notion_page_properties_updated').subBlocks,
446+
...getTrigger('notion_page_content_updated').subBlocks,
447+
...getTrigger('notion_page_deleted').subBlocks,
448+
...getTrigger('notion_database_created').subBlocks,
449+
...getTrigger('notion_database_schema_updated').subBlocks,
450+
...getTrigger('notion_database_deleted').subBlocks,
451+
...getTrigger('notion_comment_created').subBlocks,
452+
...getTrigger('notion_webhook').subBlocks,
453+
],
454+
triggers: {
455+
enabled: true,
456+
available: [
457+
'notion_page_created',
458+
'notion_page_properties_updated',
459+
'notion_page_content_updated',
460+
'notion_page_deleted',
461+
'notion_database_created',
462+
'notion_database_schema_updated',
463+
'notion_database_deleted',
464+
'notion_comment_created',
465+
'notion_webhook',
466+
],
467+
},
440468
tools: {
441469
access: [
442470
'notion_read_v2',
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import crypto from 'crypto'
2+
import { createLogger } from '@sim/logger'
3+
import { NextResponse } from 'next/server'
4+
import { safeCompare } from '@/lib/core/security/encryption'
5+
import type {
6+
EventMatchContext,
7+
FormatInputContext,
8+
FormatInputResult,
9+
WebhookProviderHandler,
10+
} from '@/lib/webhooks/providers/types'
11+
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
12+
13+
const logger = createLogger('WebhookProvider:Notion')
14+
15+
/**
16+
* Validates a Notion webhook signature using HMAC SHA-256.
17+
* Notion sends X-Notion-Signature as "sha256=<hex>".
18+
*/
19+
function validateNotionSignature(secret: string, signature: string, body: string): boolean {
20+
try {
21+
if (!secret || !signature || !body) {
22+
logger.warn('Notion signature validation missing required fields', {
23+
hasSecret: !!secret,
24+
hasSignature: !!signature,
25+
hasBody: !!body,
26+
})
27+
return false
28+
}
29+
30+
const providedHash = signature.startsWith('sha256=') ? signature.slice(7) : signature
31+
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
32+
33+
logger.debug('Notion signature comparison', {
34+
computedSignature: `${computedHash.substring(0, 10)}...`,
35+
providedSignature: `${providedHash.substring(0, 10)}...`,
36+
computedLength: computedHash.length,
37+
providedLength: providedHash.length,
38+
match: computedHash === providedHash,
39+
})
40+
41+
return safeCompare(computedHash, providedHash)
42+
} catch (error) {
43+
logger.error('Error validating Notion signature:', error)
44+
return false
45+
}
46+
}
47+
48+
export const notionHandler: WebhookProviderHandler = {
49+
verifyAuth: createHmacVerifier({
50+
configKey: 'webhookSecret',
51+
headerName: 'X-Notion-Signature',
52+
validateFn: validateNotionSignature,
53+
providerLabel: 'Notion',
54+
}),
55+
56+
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
57+
const b = body as Record<string, unknown>
58+
return {
59+
input: {
60+
id: b.id,
61+
type: b.type,
62+
timestamp: b.timestamp,
63+
workspace_id: b.workspace_id,
64+
workspace_name: b.workspace_name,
65+
subscription_id: b.subscription_id,
66+
integration_id: b.integration_id,
67+
attempt_number: b.attempt_number,
68+
authors: b.authors || [],
69+
entity: b.entity || {},
70+
data: b.data || {},
71+
},
72+
}
73+
},
74+
75+
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
76+
const triggerId = providerConfig.triggerId as string | undefined
77+
const obj = body as Record<string, unknown>
78+
79+
if (triggerId && triggerId !== 'notion_webhook') {
80+
const { isNotionPayloadMatch } = await import('@/triggers/notion/utils')
81+
if (!isNotionPayloadMatch(triggerId, obj)) {
82+
const eventType = obj.type as string | undefined
83+
logger.debug(
84+
`[${requestId}] Notion event mismatch for trigger ${triggerId}. Event: ${eventType}. Skipping execution.`,
85+
{
86+
webhookId: webhook.id,
87+
workflowId: workflow.id,
88+
triggerId,
89+
receivedEvent: eventType,
90+
}
91+
)
92+
return NextResponse.json({
93+
message: 'Event type does not match trigger configuration. Ignoring.',
94+
})
95+
}
96+
}
97+
98+
return true
99+
},
100+
}

apps/sim/lib/webhooks/providers/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { jiraHandler } from '@/lib/webhooks/providers/jira'
2323
import { lemlistHandler } from '@/lib/webhooks/providers/lemlist'
2424
import { linearHandler } from '@/lib/webhooks/providers/linear'
2525
import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams'
26+
import { notionHandler } from '@/lib/webhooks/providers/notion'
2627
import { outlookHandler } from '@/lib/webhooks/providers/outlook'
2728
import { resendHandler } from '@/lib/webhooks/providers/resend'
2829
import { rssHandler } from '@/lib/webhooks/providers/rss'
@@ -64,6 +65,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
6465
linear: linearHandler,
6566
resend: resendHandler,
6667
'microsoft-teams': microsoftTeamsHandler,
68+
notion: notionHandler,
6769
outlook: outlookHandler,
6870
rss: rssHandler,
6971
slack: slackHandler,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NotionIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildCommentEventOutputs,
5+
buildNotionExtraFields,
6+
notionSetupInstructions,
7+
notionTriggerOptions,
8+
} from '@/triggers/notion/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Notion Comment Created Trigger
13+
*/
14+
export const notionCommentCreatedTrigger: TriggerConfig = {
15+
id: 'notion_comment_created',
16+
name: 'Notion Comment Created',
17+
provider: 'notion',
18+
description: 'Trigger workflow when a comment or suggested edit is added in Notion',
19+
version: '1.0.0',
20+
icon: NotionIcon,
21+
22+
subBlocks: buildTriggerSubBlocks({
23+
triggerId: 'notion_comment_created',
24+
triggerOptions: notionTriggerOptions,
25+
setupInstructions: notionSetupInstructions('comment.created'),
26+
extraFields: buildNotionExtraFields('notion_comment_created'),
27+
}),
28+
29+
outputs: buildCommentEventOutputs(),
30+
31+
webhook: {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
'X-Notion-Signature': 'sha256=...',
36+
},
37+
},
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NotionIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildDatabaseEventOutputs,
5+
buildNotionExtraFields,
6+
notionSetupInstructions,
7+
notionTriggerOptions,
8+
} from '@/triggers/notion/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Notion Database Created Trigger
13+
*/
14+
export const notionDatabaseCreatedTrigger: TriggerConfig = {
15+
id: 'notion_database_created',
16+
name: 'Notion Database Created',
17+
provider: 'notion',
18+
description: 'Trigger workflow when a new database is created in Notion',
19+
version: '1.0.0',
20+
icon: NotionIcon,
21+
22+
subBlocks: buildTriggerSubBlocks({
23+
triggerId: 'notion_database_created',
24+
triggerOptions: notionTriggerOptions,
25+
setupInstructions: notionSetupInstructions('database.created'),
26+
extraFields: buildNotionExtraFields('notion_database_created'),
27+
}),
28+
29+
outputs: buildDatabaseEventOutputs(),
30+
31+
webhook: {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
'X-Notion-Signature': 'sha256=...',
36+
},
37+
},
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NotionIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildDatabaseEventOutputs,
5+
buildNotionExtraFields,
6+
notionSetupInstructions,
7+
notionTriggerOptions,
8+
} from '@/triggers/notion/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Notion Database Deleted Trigger
13+
*/
14+
export const notionDatabaseDeletedTrigger: TriggerConfig = {
15+
id: 'notion_database_deleted',
16+
name: 'Notion Database Deleted',
17+
provider: 'notion',
18+
description: 'Trigger workflow when a database is deleted in Notion',
19+
version: '1.0.0',
20+
icon: NotionIcon,
21+
22+
subBlocks: buildTriggerSubBlocks({
23+
triggerId: 'notion_database_deleted',
24+
triggerOptions: notionTriggerOptions,
25+
setupInstructions: notionSetupInstructions('database.deleted'),
26+
extraFields: buildNotionExtraFields('notion_database_deleted'),
27+
}),
28+
29+
outputs: buildDatabaseEventOutputs(),
30+
31+
webhook: {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
'X-Notion-Signature': 'sha256=...',
36+
},
37+
},
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NotionIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildDatabaseEventOutputs,
5+
buildNotionExtraFields,
6+
notionSetupInstructions,
7+
notionTriggerOptions,
8+
} from '@/triggers/notion/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Notion Database Schema Updated Trigger
13+
*
14+
* Fires when a database schema (properties/columns) is modified.
15+
*/
16+
export const notionDatabaseSchemaUpdatedTrigger: TriggerConfig = {
17+
id: 'notion_database_schema_updated',
18+
name: 'Notion Database Schema Updated',
19+
provider: 'notion',
20+
description: 'Trigger workflow when a database schema is modified in Notion',
21+
version: '1.0.0',
22+
icon: NotionIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'notion_database_schema_updated',
26+
triggerOptions: notionTriggerOptions,
27+
setupInstructions: notionSetupInstructions('database.schema_updated'),
28+
extraFields: buildNotionExtraFields('notion_database_schema_updated'),
29+
}),
30+
31+
outputs: buildDatabaseEventOutputs(),
32+
33+
webhook: {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
'X-Notion-Signature': 'sha256=...',
38+
},
39+
},
40+
}

apps/sim/triggers/notion/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { notionCommentCreatedTrigger } from './comment_created'
2+
export { notionDatabaseCreatedTrigger } from './database_created'
3+
export { notionDatabaseDeletedTrigger } from './database_deleted'
4+
export { notionDatabaseSchemaUpdatedTrigger } from './database_schema_updated'
5+
export { notionPageContentUpdatedTrigger } from './page_content_updated'
6+
export { notionPageCreatedTrigger } from './page_created'
7+
export { notionPageDeletedTrigger } from './page_deleted'
8+
export { notionPagePropertiesUpdatedTrigger } from './page_properties_updated'
9+
export { notionWebhookTrigger } from './webhook'

0 commit comments

Comments
 (0)