From baa3dcf85b4c52d4d3893a7c3549c5fc587c7b1f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:33:27 -0700 Subject: [PATCH 1/4] feat(trigger): add ServiceNow webhook triggers --- apps/sim/blocks/blocks/servicenow.ts | 17 +++ apps/sim/triggers/registry.ts | 12 ++ .../servicenow/change_request_created.ts | 37 +++++ .../servicenow/change_request_updated.ts | 37 +++++ .../triggers/servicenow/incident_created.ts | 40 ++++++ .../triggers/servicenow/incident_updated.ts | 37 +++++ apps/sim/triggers/servicenow/index.ts | 5 + apps/sim/triggers/servicenow/utils.ts | 126 ++++++++++++++++++ apps/sim/triggers/servicenow/webhook.ts | 38 ++++++ 9 files changed, 349 insertions(+) create mode 100644 apps/sim/triggers/servicenow/change_request_created.ts create mode 100644 apps/sim/triggers/servicenow/change_request_updated.ts create mode 100644 apps/sim/triggers/servicenow/incident_created.ts create mode 100644 apps/sim/triggers/servicenow/incident_updated.ts create mode 100644 apps/sim/triggers/servicenow/index.ts create mode 100644 apps/sim/triggers/servicenow/utils.ts create mode 100644 apps/sim/triggers/servicenow/webhook.ts diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 14376584725..760365d11ec 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -2,6 +2,7 @@ import { ServiceNowIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { IntegrationType } from '@/blocks/types' import type { ServiceNowResponse } from '@/tools/servicenow/types' +import { getTrigger } from '@/triggers' export const ServiceNowBlock: BlockConfig = { type: 'servicenow', @@ -215,6 +216,12 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st condition: { field: 'operation', value: 'servicenow_delete_record' }, required: true, }, + // Trigger SubBlocks + ...getTrigger('servicenow_incident_created').subBlocks, + ...getTrigger('servicenow_incident_updated').subBlocks, + ...getTrigger('servicenow_change_request_created').subBlocks, + ...getTrigger('servicenow_change_request_updated').subBlocks, + ...getTrigger('servicenow_webhook').subBlocks, ], tools: { access: [ @@ -262,4 +269,14 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st success: { type: 'boolean', description: 'Operation success status' }, metadata: { type: 'json', description: 'Operation metadata' }, }, + triggers: { + enabled: true, + available: [ + 'servicenow_incident_created', + 'servicenow_incident_updated', + 'servicenow_change_request_created', + 'servicenow_change_request_updated', + 'servicenow_webhook', + ], + }, } diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 6db1582457c..1e7cf2b3c8b 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -235,6 +235,13 @@ import { salesforceRecordUpdatedTrigger, salesforceWebhookTrigger, } from '@/triggers/salesforce' +import { + servicenowChangeRequestCreatedTrigger, + servicenowChangeRequestUpdatedTrigger, + servicenowIncidentCreatedTrigger, + servicenowIncidentUpdatedTrigger, + servicenowWebhookTrigger, +} from '@/triggers/servicenow' import { slackWebhookTrigger } from '@/triggers/slack' import { stripeWebhookTrigger } from '@/triggers/stripe' import { telegramWebhookTrigger } from '@/triggers/telegram' @@ -437,6 +444,11 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { salesforce_opportunity_stage_changed: salesforceOpportunityStageChangedTrigger, salesforce_case_status_changed: salesforceCaseStatusChangedTrigger, salesforce_webhook: salesforceWebhookTrigger, + servicenow_incident_created: servicenowIncidentCreatedTrigger, + servicenow_incident_updated: servicenowIncidentUpdatedTrigger, + servicenow_change_request_created: servicenowChangeRequestCreatedTrigger, + servicenow_change_request_updated: servicenowChangeRequestUpdatedTrigger, + servicenow_webhook: servicenowWebhookTrigger, stripe_webhook: stripeWebhookTrigger, telegram_webhook: telegramWebhookTrigger, typeform_webhook: typeformWebhookTrigger, diff --git a/apps/sim/triggers/servicenow/change_request_created.ts b/apps/sim/triggers/servicenow/change_request_created.ts new file mode 100644 index 00000000000..bd538158dd7 --- /dev/null +++ b/apps/sim/triggers/servicenow/change_request_created.ts @@ -0,0 +1,37 @@ +import { ServiceNowIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildChangeRequestOutputs, + buildServiceNowExtraFields, + servicenowSetupInstructions, + servicenowTriggerOptions, +} from '@/triggers/servicenow/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * ServiceNow Change Request Created Trigger + */ +export const servicenowChangeRequestCreatedTrigger: TriggerConfig = { + id: 'servicenow_change_request_created', + name: 'ServiceNow Change Request Created', + provider: 'servicenow', + description: 'Trigger workflow when a new change request is created in ServiceNow', + version: '1.0.0', + icon: ServiceNowIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'servicenow_change_request_created', + triggerOptions: servicenowTriggerOptions, + setupInstructions: servicenowSetupInstructions('Insert (record creation)'), + extraFields: buildServiceNowExtraFields('servicenow_change_request_created'), + }), + + outputs: buildChangeRequestOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/servicenow/change_request_updated.ts b/apps/sim/triggers/servicenow/change_request_updated.ts new file mode 100644 index 00000000000..f7148f90583 --- /dev/null +++ b/apps/sim/triggers/servicenow/change_request_updated.ts @@ -0,0 +1,37 @@ +import { ServiceNowIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildChangeRequestOutputs, + buildServiceNowExtraFields, + servicenowSetupInstructions, + servicenowTriggerOptions, +} from '@/triggers/servicenow/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * ServiceNow Change Request Updated Trigger + */ +export const servicenowChangeRequestUpdatedTrigger: TriggerConfig = { + id: 'servicenow_change_request_updated', + name: 'ServiceNow Change Request Updated', + provider: 'servicenow', + description: 'Trigger workflow when a change request is updated in ServiceNow', + version: '1.0.0', + icon: ServiceNowIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'servicenow_change_request_updated', + triggerOptions: servicenowTriggerOptions, + setupInstructions: servicenowSetupInstructions('Update (record modification)'), + extraFields: buildServiceNowExtraFields('servicenow_change_request_updated'), + }), + + outputs: buildChangeRequestOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/servicenow/incident_created.ts b/apps/sim/triggers/servicenow/incident_created.ts new file mode 100644 index 00000000000..170ab357296 --- /dev/null +++ b/apps/sim/triggers/servicenow/incident_created.ts @@ -0,0 +1,40 @@ +import { ServiceNowIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildIncidentOutputs, + buildServiceNowExtraFields, + servicenowSetupInstructions, + servicenowTriggerOptions, +} from '@/triggers/servicenow/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * ServiceNow Incident Created Trigger + * + * Primary trigger — includes the dropdown for selecting trigger type. + */ +export const servicenowIncidentCreatedTrigger: TriggerConfig = { + id: 'servicenow_incident_created', + name: 'ServiceNow Incident Created', + provider: 'servicenow', + description: 'Trigger workflow when a new incident is created in ServiceNow', + version: '1.0.0', + icon: ServiceNowIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'servicenow_incident_created', + triggerOptions: servicenowTriggerOptions, + includeDropdown: true, + setupInstructions: servicenowSetupInstructions('Insert (record creation)'), + extraFields: buildServiceNowExtraFields('servicenow_incident_created'), + }), + + outputs: buildIncidentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/servicenow/incident_updated.ts b/apps/sim/triggers/servicenow/incident_updated.ts new file mode 100644 index 00000000000..70c9914d6a6 --- /dev/null +++ b/apps/sim/triggers/servicenow/incident_updated.ts @@ -0,0 +1,37 @@ +import { ServiceNowIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildIncidentOutputs, + buildServiceNowExtraFields, + servicenowSetupInstructions, + servicenowTriggerOptions, +} from '@/triggers/servicenow/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * ServiceNow Incident Updated Trigger + */ +export const servicenowIncidentUpdatedTrigger: TriggerConfig = { + id: 'servicenow_incident_updated', + name: 'ServiceNow Incident Updated', + provider: 'servicenow', + description: 'Trigger workflow when an incident is updated in ServiceNow', + version: '1.0.0', + icon: ServiceNowIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'servicenow_incident_updated', + triggerOptions: servicenowTriggerOptions, + setupInstructions: servicenowSetupInstructions('Update (record modification)'), + extraFields: buildServiceNowExtraFields('servicenow_incident_updated'), + }), + + outputs: buildIncidentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/servicenow/index.ts b/apps/sim/triggers/servicenow/index.ts new file mode 100644 index 00000000000..adb585ff29b --- /dev/null +++ b/apps/sim/triggers/servicenow/index.ts @@ -0,0 +1,5 @@ +export { servicenowChangeRequestCreatedTrigger } from './change_request_created' +export { servicenowChangeRequestUpdatedTrigger } from './change_request_updated' +export { servicenowIncidentCreatedTrigger } from './incident_created' +export { servicenowIncidentUpdatedTrigger } from './incident_updated' +export { servicenowWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/servicenow/utils.ts b/apps/sim/triggers/servicenow/utils.ts new file mode 100644 index 00000000000..92b43b59556 --- /dev/null +++ b/apps/sim/triggers/servicenow/utils.ts @@ -0,0 +1,126 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Shared trigger dropdown options for all ServiceNow triggers + */ +export const servicenowTriggerOptions = [ + { label: 'Incident Created', id: 'servicenow_incident_created' }, + { label: 'Incident Updated', id: 'servicenow_incident_updated' }, + { label: 'Change Request Created', id: 'servicenow_change_request_created' }, + { label: 'Change Request Updated', id: 'servicenow_change_request_updated' }, + { label: 'Generic Webhook (All Events)', id: 'servicenow_webhook' }, +] + +/** + * Generates setup instructions for ServiceNow webhooks. + * ServiceNow uses Business Rules with RESTMessageV2 for outbound webhooks. + */ +export function servicenowSetupInstructions(eventType: string): string { + const instructions = [ + 'Note: You need admin or developer permissions in your ServiceNow instance to create Business Rules.', + 'Navigate to System Definition > Business Rules and create a new Business Rule.', + `Set the table (e.g., incident, change_request), set When to after, and check ${eventType}.`, + 'Check the Advanced checkbox to enable the script editor.', + `In the script, use RESTMessageV2 to POST the record data as JSON to the Webhook URL above. Example:
var r = new sn_ws.RESTMessageV2();\nr.setEndpoint("<webhook_url>");\nr.setHttpMethod("POST");\nr.setRequestHeader("Content-Type", "application/json");\nr.setRequestBody(JSON.stringify({\n sysId: current.sys_id.toString(),\n number: current.number.toString(),\n shortDescription: current.short_description.toString(),\n state: current.state.toString(),\n priority: current.priority.toString()\n}));\nr.execute();`, + 'Activate the Business Rule and click "Save" above to activate your trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index === 0 ? instruction : `${index}. ${instruction}`}
` + ) + .join('') +} + +/** + * Extra fields for ServiceNow triggers (optional table filter) + */ +export function buildServiceNowExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'tableName', + title: 'Table Name (Optional)', + type: 'short-input', + placeholder: 'e.g., incident, change_request', + description: 'Optionally filter to a specific ServiceNow table', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Common record fields shared across ServiceNow trigger outputs + */ +function buildRecordOutputs(): Record { + return { + sysId: { type: 'string', description: 'Unique system ID of the record' }, + number: { type: 'string', description: 'Record number (e.g., INC0010001, CHG0010001)' }, + tableName: { type: 'string', description: 'ServiceNow table name' }, + shortDescription: { type: 'string', description: 'Short description of the record' }, + description: { type: 'string', description: 'Full description of the record' }, + state: { type: 'string', description: 'Current state of the record' }, + priority: { + type: 'string', + description: 'Priority level (1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning)', + }, + assignedTo: { type: 'string', description: 'User assigned to this record' }, + assignmentGroup: { type: 'string', description: 'Group assigned to this record' }, + createdBy: { type: 'string', description: 'User who created the record' }, + createdOn: { type: 'string', description: 'When the record was created (ISO 8601)' }, + updatedBy: { type: 'string', description: 'User who last updated the record' }, + updatedOn: { type: 'string', description: 'When the record was last updated (ISO 8601)' }, + } +} + +/** + * Outputs for incident triggers + */ +export function buildIncidentOutputs(): Record { + return { + ...buildRecordOutputs(), + urgency: { type: 'string', description: 'Urgency level (1=High, 2=Medium, 3=Low)' }, + impact: { type: 'string', description: 'Impact level (1=High, 2=Medium, 3=Low)' }, + category: { type: 'string', description: 'Incident category' }, + subcategory: { type: 'string', description: 'Incident subcategory' }, + caller: { type: 'string', description: 'Caller/requester of the incident' }, + resolvedBy: { type: 'string', description: 'User who resolved the incident' }, + resolvedAt: { type: 'string', description: 'When the incident was resolved' }, + closeNotes: { type: 'string', description: 'Notes added when the incident was closed' }, + record: { type: 'json', description: 'Full incident record data' }, + } +} + +/** + * Outputs for change request triggers + */ +export function buildChangeRequestOutputs(): Record { + return { + ...buildRecordOutputs(), + type: { type: 'string', description: 'Change type (Normal, Standard, Emergency)' }, + risk: { type: 'string', description: 'Risk level of the change' }, + impact: { type: 'string', description: 'Impact level of the change' }, + approval: { type: 'string', description: 'Approval status' }, + startDate: { type: 'string', description: 'Planned start date' }, + endDate: { type: 'string', description: 'Planned end date' }, + category: { type: 'string', description: 'Change category' }, + record: { type: 'json', description: 'Full change request record data' }, + } +} + +/** + * Outputs for the generic webhook trigger (all events) + */ +export function buildServiceNowWebhookOutputs(): Record { + return { + ...buildRecordOutputs(), + eventType: { + type: 'string', + description: 'The type of event that triggered this workflow (e.g., insert, update, delete)', + }, + category: { type: 'string', description: 'Record category' }, + record: { type: 'json', description: 'Full record data from the webhook payload' }, + } +} diff --git a/apps/sim/triggers/servicenow/webhook.ts b/apps/sim/triggers/servicenow/webhook.ts new file mode 100644 index 00000000000..7cb1d19d5d7 --- /dev/null +++ b/apps/sim/triggers/servicenow/webhook.ts @@ -0,0 +1,38 @@ +import { ServiceNowIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildServiceNowExtraFields, + buildServiceNowWebhookOutputs, + servicenowSetupInstructions, + servicenowTriggerOptions, +} from '@/triggers/servicenow/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Generic ServiceNow Webhook Trigger + * Captures all ServiceNow webhook events + */ +export const servicenowWebhookTrigger: TriggerConfig = { + id: 'servicenow_webhook', + name: 'ServiceNow Webhook (All Events)', + provider: 'servicenow', + description: 'Trigger workflow on any ServiceNow webhook event', + version: '1.0.0', + icon: ServiceNowIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'servicenow_webhook', + triggerOptions: servicenowTriggerOptions, + setupInstructions: servicenowSetupInstructions('Insert, Update, or Delete'), + extraFields: buildServiceNowExtraFields('servicenow_webhook'), + }), + + outputs: buildServiceNowWebhookOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} From bb3aee32d887c9aa0e0961dcc79d7b1e44951ba6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:41:59 -0700 Subject: [PATCH 2/4] fix(trigger): add webhook secret field and remove non-TSDoc comment Add webhookSecret field to ServiceNow triggers (matching Salesforce pattern) so users are prompted to protect the webhook endpoint. Update setup instructions to include Authorization header in the Business Rule example. Remove non-TSDoc inline comment in the block config. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/servicenow.ts | 1 - apps/sim/triggers/servicenow/utils.ts | 24 ++++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 760365d11ec..06e7249f302 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -216,7 +216,6 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st condition: { field: 'operation', value: 'servicenow_delete_record' }, required: true, }, - // Trigger SubBlocks ...getTrigger('servicenow_incident_created').subBlocks, ...getTrigger('servicenow_incident_updated').subBlocks, ...getTrigger('servicenow_change_request_created').subBlocks, diff --git a/apps/sim/triggers/servicenow/utils.ts b/apps/sim/triggers/servicenow/utils.ts index 92b43b59556..438487a0afd 100644 --- a/apps/sim/triggers/servicenow/utils.ts +++ b/apps/sim/triggers/servicenow/utils.ts @@ -22,7 +22,8 @@ export function servicenowSetupInstructions(eventType: string): string { 'Navigate to System Definition > Business Rules and create a new Business Rule.', `Set the table (e.g., incident, change_request), set When to after, and check ${eventType}.`, 'Check the Advanced checkbox to enable the script editor.', - `In the script, use RESTMessageV2 to POST the record data as JSON to the Webhook URL above. Example:
var r = new sn_ws.RESTMessageV2();\nr.setEndpoint("<webhook_url>");\nr.setHttpMethod("POST");\nr.setRequestHeader("Content-Type", "application/json");\nr.setRequestBody(JSON.stringify({\n sysId: current.sys_id.toString(),\n number: current.number.toString(),\n shortDescription: current.short_description.toString(),\n state: current.state.toString(),\n priority: current.priority.toString()\n}));\nr.execute();`, + 'Copy the Webhook URL above and generate a Webhook Secret (any strong random string). Paste the secret in the Webhook Secret field here.', + `In the script, use RESTMessageV2 to POST the record data as JSON to the Webhook URL above. Include the secret as Authorization: Bearer <your secret> or X-Sim-Webhook-Secret: <your secret>. Example:
var r = new sn_ws.RESTMessageV2();\nr.setEndpoint("<webhook_url>");\nr.setHttpMethod("POST");\nr.setRequestHeader("Content-Type", "application/json");\nr.setRequestHeader("Authorization", "Bearer <your_webhook_secret>");\nr.setRequestBody(JSON.stringify({\n sysId: current.sys_id.toString(),\n number: current.number.toString(),\n shortDescription: current.short_description.toString(),\n state: current.state.toString(),\n priority: current.priority.toString()\n}));\nr.execute();`, 'Activate the Business Rule and click "Save" above to activate your trigger.', ] @@ -35,10 +36,29 @@ export function servicenowSetupInstructions(eventType: string): string { } /** - * Extra fields for ServiceNow triggers (optional table filter) + * Webhook secret field for ServiceNow triggers + */ +function servicenowWebhookSecretField(triggerId: string): SubBlockConfig { + return { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Generate a secret and paste it here', + description: + 'Required. Use the same value in your ServiceNow Business Rule as Bearer token or X-Sim-Webhook-Secret.', + password: true, + required: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + } +} + +/** + * Extra fields for ServiceNow triggers (webhook secret + optional table filter) */ export function buildServiceNowExtraFields(triggerId: string): SubBlockConfig[] { return [ + servicenowWebhookSecretField(triggerId), { id: 'tableName', title: 'Table Name (Optional)', From 77712e67af46aec734360d8cfb65c05a15331010 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:44:27 -0700 Subject: [PATCH 3/4] feat(trigger): add ServiceNow provider handler with event matching Add dedicated ServiceNow webhook provider handler with: - verifyAuth: validates webhookSecret via Bearer token or X-Sim-Webhook-Secret - matchEvent: filters events by trigger type and table name using isServiceNowEventMatch utility (matching Salesforce/GitHub pattern) The event matcher handles incident created/updated and change request created/updated triggers with table name enforcement and event type normalization. The generic webhook trigger passes through all events but still respects the optional table name filter. Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/lib/webhooks/providers/servicenow.ts | 53 +++++++ apps/sim/triggers/servicenow/utils.ts | 131 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 apps/sim/lib/webhooks/providers/servicenow.ts diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 789546a755b..332add6598f 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -28,6 +28,7 @@ import { outlookHandler } from '@/lib/webhooks/providers/outlook' import { resendHandler } from '@/lib/webhooks/providers/resend' import { rssHandler } from '@/lib/webhooks/providers/rss' import { salesforceHandler } from '@/lib/webhooks/providers/salesforce' +import { servicenowHandler } from '@/lib/webhooks/providers/servicenow' import { slackHandler } from '@/lib/webhooks/providers/slack' import { stripeHandler } from '@/lib/webhooks/providers/stripe' import { telegramHandler } from '@/lib/webhooks/providers/telegram' @@ -72,6 +73,7 @@ const PROVIDER_HANDLERS: Record = { outlook: outlookHandler, rss: rssHandler, salesforce: salesforceHandler, + servicenow: servicenowHandler, slack: slackHandler, stripe: stripeHandler, telegram: telegramHandler, diff --git a/apps/sim/lib/webhooks/providers/servicenow.ts b/apps/sim/lib/webhooks/providers/servicenow.ts new file mode 100644 index 00000000000..acf6e804140 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/servicenow.ts @@ -0,0 +1,53 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import type { AuthContext, EventMatchContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types' +import { verifyTokenAuth } from '@/lib/webhooks/providers/utils' + +const logger = createLogger('WebhookProvider:ServiceNow') + +function asRecord(body: unknown): Record { + return body && typeof body === 'object' && !Array.isArray(body) + ? (body as Record) + : {} +} + +export const servicenowHandler: WebhookProviderHandler = { + verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null { + const secret = providerConfig.webhookSecret as string | undefined + if (!secret?.trim()) { + logger.warn(`[${requestId}] ServiceNow webhook missing webhookSecret — rejecting`) + return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 }) + } + + if ( + !verifyTokenAuth(request, secret.trim(), 'x-sim-webhook-secret') && + !verifyTokenAuth(request, secret.trim()) + ) { + logger.warn(`[${requestId}] ServiceNow webhook secret verification failed`) + return new NextResponse('Unauthorized - Invalid webhook secret', { status: 401 }) + } + + return null + }, + + async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId) { + return true + } + + const { isServiceNowEventMatch } = await import('@/triggers/servicenow/utils') + const configuredTableName = providerConfig.tableName as string | undefined + const obj = asRecord(body) + + if (!isServiceNowEventMatch(triggerId, obj, configuredTableName)) { + logger.debug( + `[${requestId}] ServiceNow event mismatch for trigger ${triggerId}. Skipping execution.`, + { webhookId: webhook.id, workflowId: workflow.id, triggerId } + ) + return false + } + + return true + }, +} diff --git a/apps/sim/triggers/servicenow/utils.ts b/apps/sim/triggers/servicenow/utils.ts index 438487a0afd..5884a6bb8e5 100644 --- a/apps/sim/triggers/servicenow/utils.ts +++ b/apps/sim/triggers/servicenow/utils.ts @@ -130,6 +130,137 @@ export function buildChangeRequestOutputs(): Record { } } +function normalizeToken(s: string): string { + return s.trim().toLowerCase().replace(/[\s-]+/g, '_') +} + +/** + * Extracts the table name from a ServiceNow webhook payload. + * Business Rule scripts can send tableName in multiple formats. + */ +function extractTableName(body: Record): string | undefined { + const candidates = [body.tableName, body.table_name, body.table, body.sys_class_name] + for (const c of candidates) { + if (typeof c === 'string' && c.trim()) { + return c.trim() + } + } + return undefined +} + +/** + * Extracts the event type from a ServiceNow webhook payload. + */ +function extractEventType(body: Record): string | undefined { + const candidates = [body.eventType, body.event_type, body.action, body.operation] + for (const c of candidates) { + if (typeof c === 'string' && c.trim()) { + return c.trim() + } + } + return undefined +} + +const INCIDENT_CREATED = new Set([ + 'incident_created', + 'insert', + 'created', + 'create', + 'after_insert', + 'afterinsert', +]) + +const INCIDENT_UPDATED = new Set([ + 'incident_updated', + 'update', + 'updated', + 'after_update', + 'afterupdate', +]) + +const CHANGE_REQUEST_CREATED = new Set([ + 'change_request_created', + 'insert', + 'created', + 'create', + 'after_insert', + 'afterinsert', +]) + +const CHANGE_REQUEST_UPDATED = new Set([ + 'change_request_updated', + 'update', + 'updated', + 'after_update', + 'afterupdate', +]) + +/** + * Checks whether a ServiceNow webhook payload matches the configured trigger. + * Used by the ServiceNow provider handler to filter events at runtime. + */ +export function isServiceNowEventMatch( + triggerId: string, + body: Record, + configuredTableName?: string +): boolean { + const payloadTable = extractTableName(body) + const eventType = extractEventType(body) + + if (triggerId === 'servicenow_webhook') { + if (!configuredTableName?.trim()) { + return true + } + if (!payloadTable) { + return true + } + return normalizeToken(payloadTable) === normalizeToken(configuredTableName) + } + + if (triggerId === 'servicenow_incident_created' || triggerId === 'servicenow_incident_updated') { + if (configuredTableName?.trim()) { + if (payloadTable && normalizeToken(payloadTable) !== normalizeToken(configuredTableName)) { + return false + } + } else if (payloadTable && normalizeToken(payloadTable) !== 'incident') { + return false + } + + if (!eventType) { + return true + } + + const normalized = normalizeToken(eventType) + return triggerId === 'servicenow_incident_created' + ? INCIDENT_CREATED.has(normalized) + : INCIDENT_UPDATED.has(normalized) + } + + if ( + triggerId === 'servicenow_change_request_created' || + triggerId === 'servicenow_change_request_updated' + ) { + if (configuredTableName?.trim()) { + if (payloadTable && normalizeToken(payloadTable) !== normalizeToken(configuredTableName)) { + return false + } + } else if (payloadTable && normalizeToken(payloadTable) !== 'change_request') { + return false + } + + if (!eventType) { + return true + } + + const normalized = normalizeToken(eventType) + return triggerId === 'servicenow_change_request_created' + ? CHANGE_REQUEST_CREATED.has(normalized) + : CHANGE_REQUEST_UPDATED.has(normalized) + } + + return true +} + /** * Outputs for the generic webhook trigger (all events) */ From 952cc690fa01506fd939a5e6068fd4efebc2b67d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:47:04 -0700 Subject: [PATCH 4/4] lint --- .../integrations/data/integrations.json | 30 +++++++++++++++++-- apps/sim/lib/webhooks/providers/servicenow.ts | 6 +++- apps/sim/triggers/servicenow/utils.ts | 5 +++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index a05fcbb7eff..13ef350765a 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -10784,8 +10784,34 @@ } ], "operationCount": 4, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "servicenow_incident_created", + "name": "ServiceNow Incident Created", + "description": "Trigger workflow when a new incident is created in ServiceNow" + }, + { + "id": "servicenow_incident_updated", + "name": "ServiceNow Incident Updated", + "description": "Trigger workflow when an incident is updated in ServiceNow" + }, + { + "id": "servicenow_change_request_created", + "name": "ServiceNow Change Request Created", + "description": "Trigger workflow when a new change request is created in ServiceNow" + }, + { + "id": "servicenow_change_request_updated", + "name": "ServiceNow Change Request Updated", + "description": "Trigger workflow when a change request is updated in ServiceNow" + }, + { + "id": "servicenow_webhook", + "name": "ServiceNow Webhook (All Events)", + "description": "Trigger workflow on any ServiceNow webhook event" + } + ], + "triggerCount": 5, "authType": "none", "category": "tools", "integrationType": "customer-support", diff --git a/apps/sim/lib/webhooks/providers/servicenow.ts b/apps/sim/lib/webhooks/providers/servicenow.ts index acf6e804140..8118bd72ed8 100644 --- a/apps/sim/lib/webhooks/providers/servicenow.ts +++ b/apps/sim/lib/webhooks/providers/servicenow.ts @@ -1,6 +1,10 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' -import type { AuthContext, EventMatchContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types' +import type { + AuthContext, + EventMatchContext, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' import { verifyTokenAuth } from '@/lib/webhooks/providers/utils' const logger = createLogger('WebhookProvider:ServiceNow') diff --git a/apps/sim/triggers/servicenow/utils.ts b/apps/sim/triggers/servicenow/utils.ts index 5884a6bb8e5..7c85b659618 100644 --- a/apps/sim/triggers/servicenow/utils.ts +++ b/apps/sim/triggers/servicenow/utils.ts @@ -131,7 +131,10 @@ export function buildChangeRequestOutputs(): Record { } function normalizeToken(s: string): string { - return s.trim().toLowerCase().replace(/[\s-]+/g, '_') + return s + .trim() + .toLowerCase() + .replace(/[\s-]+/g, '_') } /**