mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
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>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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<NotionResponse> = {
|
||||
@@ -436,7 +437,34 @@ export const NotionV2Block: BlockConfig<any> = {
|
||||
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',
|
||||
|
||||
100
apps/sim/lib/webhooks/providers/notion.ts
Normal file
100
apps/sim/lib/webhooks/providers/notion.ts
Normal file
@@ -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=<hex>".
|
||||
*/
|
||||
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<FormatInputResult> {
|
||||
const b = body as Record<string, unknown>
|
||||
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<string, unknown>
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
@@ -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<string, WebhookProviderHandler> = {
|
||||
linear: linearHandler,
|
||||
resend: resendHandler,
|
||||
'microsoft-teams': microsoftTeamsHandler,
|
||||
notion: notionHandler,
|
||||
outlook: outlookHandler,
|
||||
rss: rssHandler,
|
||||
slack: slackHandler,
|
||||
|
||||
38
apps/sim/triggers/notion/comment_created.ts
Normal file
38
apps/sim/triggers/notion/comment_created.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/notion/database_created.ts
Normal file
38
apps/sim/triggers/notion/database_created.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/notion/database_deleted.ts
Normal file
38
apps/sim/triggers/notion/database_deleted.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
40
apps/sim/triggers/notion/database_schema_updated.ts
Normal file
40
apps/sim/triggers/notion/database_schema_updated.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
9
apps/sim/triggers/notion/index.ts
Normal file
9
apps/sim/triggers/notion/index.ts
Normal file
@@ -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'
|
||||
40
apps/sim/triggers/notion/page_content_updated.ts
Normal file
40
apps/sim/triggers/notion/page_content_updated.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
41
apps/sim/triggers/notion/page_created.ts
Normal file
41
apps/sim/triggers/notion/page_created.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/notion/page_deleted.ts
Normal file
38
apps/sim/triggers/notion/page_deleted.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
40
apps/sim/triggers/notion/page_properties_updated.ts
Normal file
40
apps/sim/triggers/notion/page_properties_updated.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
201
apps/sim/triggers/notion/utils.ts
Normal file
201
apps/sim/triggers/notion/utils.ts
Normal file
@@ -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 <a href="https://www.notion.so/profile/integrations" target="_blank" rel="noopener noreferrer"><strong>notion.so/profile/integrations</strong></a> and select your integration (or create one).',
|
||||
'Navigate to the <strong>Webhooks</strong> tab.',
|
||||
'Click <strong>"Create a subscription"</strong>.',
|
||||
'Paste the <strong>Webhook URL</strong> above into the URL field.',
|
||||
`Select the <strong>${eventType}</strong> event type(s).`,
|
||||
'Notion will send a verification request. Copy the <strong>verification_token</strong> 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) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.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<string, TriggerOutput> {
|
||||
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<string, TriggerOutput> {
|
||||
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<string, TriggerOutput> {
|
||||
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<string, TriggerOutput> {
|
||||
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<string, TriggerOutput> {
|
||||
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<string, TriggerOutput> {
|
||||
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<string, string[]> = {
|
||||
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<string, unknown>): 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
|
||||
}
|
||||
38
apps/sim/triggers/notion/webhook.ts
Normal file
38
apps/sim/triggers/notion/webhook.ts
Normal file
@@ -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=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user