Skip to content

Commit 51236f0

Browse files
waleedlatif1claude
andcommitted
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>
1 parent c9b45f4 commit 51236f0

16 files changed

Lines changed: 790 additions & 3 deletions

File tree

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8138,8 +8138,54 @@
81388138
"docsUrl": "https://docs.sim.ai/tools/notion",
81398139
"operations": [],
81408140
"operationCount": 0,
8141-
"triggers": [],
8142-
"triggerCount": 0,
8141+
"triggers": [
8142+
{
8143+
"id": "notion_page_created",
8144+
"name": "Notion Page Created",
8145+
"description": "Trigger workflow when a new page is created in Notion"
8146+
},
8147+
{
8148+
"id": "notion_page_properties_updated",
8149+
"name": "Notion Page Properties Updated",
8150+
"description": "Trigger workflow when page properties are modified in Notion"
8151+
},
8152+
{
8153+
"id": "notion_page_content_updated",
8154+
"name": "Notion Page Content Updated",
8155+
"description": "Trigger workflow when page content is changed in Notion"
8156+
},
8157+
{
8158+
"id": "notion_page_deleted",
8159+
"name": "Notion Page Deleted",
8160+
"description": "Trigger workflow when a page is deleted in Notion"
8161+
},
8162+
{
8163+
"id": "notion_database_created",
8164+
"name": "Notion Database Created",
8165+
"description": "Trigger workflow when a new database is created in Notion"
8166+
},
8167+
{
8168+
"id": "notion_database_schema_updated",
8169+
"name": "Notion Database Schema Updated",
8170+
"description": "Trigger workflow when a database schema is modified in Notion"
8171+
},
8172+
{
8173+
"id": "notion_database_deleted",
8174+
"name": "Notion Database Deleted",
8175+
"description": "Trigger workflow when a database is deleted in Notion"
8176+
},
8177+
{
8178+
"id": "notion_comment_created",
8179+
"name": "Notion Comment Created",
8180+
"description": "Trigger workflow when a comment or suggested edit is added in Notion"
8181+
},
8182+
{
8183+
"id": "notion_webhook",
8184+
"name": "Notion Webhook (All Events)",
8185+
"description": "Trigger workflow on any Notion webhook event"
8186+
}
8187+
],
8188+
"triggerCount": 9,
81438189
"authType": "oauth",
81448190
"category": "tools",
81458191
"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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
AuthContext,
7+
EventMatchContext,
8+
FormatInputContext,
9+
FormatInputResult,
10+
WebhookProviderHandler,
11+
} from '@/lib/webhooks/providers/types'
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({ request, rawBody, requestId, providerConfig }: AuthContext) {
50+
const secret = providerConfig.webhookSecret as string | undefined
51+
if (!secret) {
52+
return null
53+
}
54+
55+
const signature = request.headers.get('X-Notion-Signature')
56+
if (!signature) {
57+
logger.warn(`[${requestId}] Notion webhook missing signature header`)
58+
return new NextResponse('Unauthorized - Missing Notion signature', { status: 401 })
59+
}
60+
61+
if (!validateNotionSignature(secret, signature, rawBody)) {
62+
logger.warn(`[${requestId}] Notion signature verification failed`, {
63+
signatureLength: signature.length,
64+
secretLength: secret.length,
65+
})
66+
return new NextResponse('Unauthorized - Invalid Notion signature', { status: 401 })
67+
}
68+
69+
return null
70+
},
71+
72+
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
73+
const b = body as Record<string, unknown>
74+
return {
75+
input: {
76+
id: b.id,
77+
type: b.type,
78+
timestamp: b.timestamp,
79+
workspace_id: b.workspace_id,
80+
workspace_name: b.workspace_name,
81+
subscription_id: b.subscription_id,
82+
integration_id: b.integration_id,
83+
attempt_number: b.attempt_number,
84+
authors: b.authors || [],
85+
entity: b.entity || {},
86+
data: b.data || {},
87+
},
88+
}
89+
},
90+
91+
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
92+
const triggerId = providerConfig.triggerId as string | undefined
93+
const obj = body as Record<string, unknown>
94+
95+
if (triggerId && triggerId !== 'notion_webhook') {
96+
const { isNotionPayloadMatch } = await import('@/triggers/notion/utils')
97+
if (!isNotionPayloadMatch(triggerId, obj)) {
98+
const eventType = obj.type as string | undefined
99+
logger.debug(
100+
`[${requestId}] Notion event mismatch for trigger ${triggerId}. Event: ${eventType}. Skipping execution.`,
101+
{
102+
webhookId: webhook.id,
103+
workflowId: workflow.id,
104+
triggerId,
105+
receivedEvent: eventType,
106+
}
107+
)
108+
return NextResponse.json({
109+
message: 'Event type does not match trigger configuration. Ignoring.',
110+
})
111+
}
112+
}
113+
114+
return true
115+
},
116+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { jiraHandler } from '@/lib/webhooks/providers/jira'
2020
import { lemlistHandler } from '@/lib/webhooks/providers/lemlist'
2121
import { linearHandler } from '@/lib/webhooks/providers/linear'
2222
import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams'
23+
import { notionHandler } from '@/lib/webhooks/providers/notion'
2324
import { outlookHandler } from '@/lib/webhooks/providers/outlook'
2425
import { rssHandler } from '@/lib/webhooks/providers/rss'
2526
import { slackHandler } from '@/lib/webhooks/providers/slack'
@@ -56,6 +57,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
5657
lemlist: lemlistHandler,
5758
linear: linearHandler,
5859
'microsoft-teams': microsoftTeamsHandler,
60+
notion: notionHandler,
5961
outlook: outlookHandler,
6062
rss: rssHandler,
6163
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+
}

0 commit comments

Comments
 (0)