Skip to content
Merged
Prev Previous commit
Next Next commit
feat(trigger): add ServiceNow webhook triggers (#4077)
* feat(trigger): add ServiceNow webhook triggers

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* lint

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
waleedlatif1 and claude authored Apr 9, 2026
commit fc3e762b1f85506b89a2283839d938a966d3ee08
30 changes: 28 additions & 2 deletions apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -10796,8 +10796,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",
Expand Down
16 changes: 16 additions & 0 deletions apps/sim/blocks/blocks/servicenow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServiceNowResponse> = {
type: 'servicenow',
Expand Down Expand Up @@ -215,6 +216,11 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
condition: { field: 'operation', value: 'servicenow_delete_record' },
required: true,
},
...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: [
Expand Down Expand Up @@ -262,4 +268,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',
],
},
}
2 changes: 2 additions & 0 deletions apps/sim/lib/webhooks/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -72,6 +73,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
outlook: outlookHandler,
rss: rssHandler,
salesforce: salesforceHandler,
servicenow: servicenowHandler,
slack: slackHandler,
stripe: stripeHandler,
telegram: telegramHandler,
Expand Down
57 changes: 57 additions & 0 deletions apps/sim/lib/webhooks/providers/servicenow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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<string, unknown> {
return body && typeof body === 'object' && !Array.isArray(body)
? (body as Record<string, unknown>)
: {}
}

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
},
}
12 changes: 12 additions & 0 deletions apps/sim/triggers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 37 additions & 0 deletions apps/sim/triggers/servicenow/change_request_created.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
}
37 changes: 37 additions & 0 deletions apps/sim/triggers/servicenow/change_request_updated.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
}
40 changes: 40 additions & 0 deletions apps/sim/triggers/servicenow/incident_created.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
}
37 changes: 37 additions & 0 deletions apps/sim/triggers/servicenow/incident_updated.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
}
5 changes: 5 additions & 0 deletions apps/sim/triggers/servicenow/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Loading