Skip to content

Commit 9f032d9

Browse files
waleedlatif1claude
andcommitted
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>
1 parent 7391cd1 commit 9f032d9

File tree

12 files changed

+589
-2
lines changed

12 files changed

+589
-2
lines changed

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10796,8 +10796,34 @@
1079610796
}
1079710797
],
1079810798
"operationCount": 4,
10799-
"triggers": [],
10800-
"triggerCount": 0,
10799+
"triggers": [
10800+
{
10801+
"id": "servicenow_incident_created",
10802+
"name": "ServiceNow Incident Created",
10803+
"description": "Trigger workflow when a new incident is created in ServiceNow"
10804+
},
10805+
{
10806+
"id": "servicenow_incident_updated",
10807+
"name": "ServiceNow Incident Updated",
10808+
"description": "Trigger workflow when an incident is updated in ServiceNow"
10809+
},
10810+
{
10811+
"id": "servicenow_change_request_created",
10812+
"name": "ServiceNow Change Request Created",
10813+
"description": "Trigger workflow when a new change request is created in ServiceNow"
10814+
},
10815+
{
10816+
"id": "servicenow_change_request_updated",
10817+
"name": "ServiceNow Change Request Updated",
10818+
"description": "Trigger workflow when a change request is updated in ServiceNow"
10819+
},
10820+
{
10821+
"id": "servicenow_webhook",
10822+
"name": "ServiceNow Webhook (All Events)",
10823+
"description": "Trigger workflow on any ServiceNow webhook event"
10824+
}
10825+
],
10826+
"triggerCount": 5,
1080110827
"authType": "none",
1080210828
"category": "tools",
1080310829
"integrationType": "customer-support",

apps/sim/blocks/blocks/servicenow.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ServiceNowIcon } from '@/components/icons'
22
import type { BlockConfig } from '@/blocks/types'
33
import { IntegrationType } from '@/blocks/types'
44
import type { ServiceNowResponse } from '@/tools/servicenow/types'
5+
import { getTrigger } from '@/triggers'
56

67
export const ServiceNowBlock: BlockConfig<ServiceNowResponse> = {
78
type: 'servicenow',
@@ -215,6 +216,11 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
215216
condition: { field: 'operation', value: 'servicenow_delete_record' },
216217
required: true,
217218
},
219+
...getTrigger('servicenow_incident_created').subBlocks,
220+
...getTrigger('servicenow_incident_updated').subBlocks,
221+
...getTrigger('servicenow_change_request_created').subBlocks,
222+
...getTrigger('servicenow_change_request_updated').subBlocks,
223+
...getTrigger('servicenow_webhook').subBlocks,
218224
],
219225
tools: {
220226
access: [
@@ -262,4 +268,14 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
262268
success: { type: 'boolean', description: 'Operation success status' },
263269
metadata: { type: 'json', description: 'Operation metadata' },
264270
},
271+
triggers: {
272+
enabled: true,
273+
available: [
274+
'servicenow_incident_created',
275+
'servicenow_incident_updated',
276+
'servicenow_change_request_created',
277+
'servicenow_change_request_updated',
278+
'servicenow_webhook',
279+
],
280+
},
265281
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { outlookHandler } from '@/lib/webhooks/providers/outlook'
2828
import { resendHandler } from '@/lib/webhooks/providers/resend'
2929
import { rssHandler } from '@/lib/webhooks/providers/rss'
3030
import { salesforceHandler } from '@/lib/webhooks/providers/salesforce'
31+
import { servicenowHandler } from '@/lib/webhooks/providers/servicenow'
3132
import { slackHandler } from '@/lib/webhooks/providers/slack'
3233
import { stripeHandler } from '@/lib/webhooks/providers/stripe'
3334
import { telegramHandler } from '@/lib/webhooks/providers/telegram'
@@ -72,6 +73,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
7273
outlook: outlookHandler,
7374
rss: rssHandler,
7475
salesforce: salesforceHandler,
76+
servicenow: servicenowHandler,
7577
slack: slackHandler,
7678
stripe: stripeHandler,
7779
telegram: telegramHandler,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createLogger } from '@sim/logger'
2+
import { NextResponse } from 'next/server'
3+
import type {
4+
AuthContext,
5+
EventMatchContext,
6+
WebhookProviderHandler,
7+
} from '@/lib/webhooks/providers/types'
8+
import { verifyTokenAuth } from '@/lib/webhooks/providers/utils'
9+
10+
const logger = createLogger('WebhookProvider:ServiceNow')
11+
12+
function asRecord(body: unknown): Record<string, unknown> {
13+
return body && typeof body === 'object' && !Array.isArray(body)
14+
? (body as Record<string, unknown>)
15+
: {}
16+
}
17+
18+
export const servicenowHandler: WebhookProviderHandler = {
19+
verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null {
20+
const secret = providerConfig.webhookSecret as string | undefined
21+
if (!secret?.trim()) {
22+
logger.warn(`[${requestId}] ServiceNow webhook missing webhookSecret — rejecting`)
23+
return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 })
24+
}
25+
26+
if (
27+
!verifyTokenAuth(request, secret.trim(), 'x-sim-webhook-secret') &&
28+
!verifyTokenAuth(request, secret.trim())
29+
) {
30+
logger.warn(`[${requestId}] ServiceNow webhook secret verification failed`)
31+
return new NextResponse('Unauthorized - Invalid webhook secret', { status: 401 })
32+
}
33+
34+
return null
35+
},
36+
37+
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
38+
const triggerId = providerConfig.triggerId as string | undefined
39+
if (!triggerId) {
40+
return true
41+
}
42+
43+
const { isServiceNowEventMatch } = await import('@/triggers/servicenow/utils')
44+
const configuredTableName = providerConfig.tableName as string | undefined
45+
const obj = asRecord(body)
46+
47+
if (!isServiceNowEventMatch(triggerId, obj, configuredTableName)) {
48+
logger.debug(
49+
`[${requestId}] ServiceNow event mismatch for trigger ${triggerId}. Skipping execution.`,
50+
{ webhookId: webhook.id, workflowId: workflow.id, triggerId }
51+
)
52+
return false
53+
}
54+
55+
return true
56+
},
57+
}

apps/sim/triggers/registry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@ import {
235235
salesforceRecordUpdatedTrigger,
236236
salesforceWebhookTrigger,
237237
} from '@/triggers/salesforce'
238+
import {
239+
servicenowChangeRequestCreatedTrigger,
240+
servicenowChangeRequestUpdatedTrigger,
241+
servicenowIncidentCreatedTrigger,
242+
servicenowIncidentUpdatedTrigger,
243+
servicenowWebhookTrigger,
244+
} from '@/triggers/servicenow'
238245
import { slackWebhookTrigger } from '@/triggers/slack'
239246
import { stripeWebhookTrigger } from '@/triggers/stripe'
240247
import { telegramWebhookTrigger } from '@/triggers/telegram'
@@ -437,6 +444,11 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
437444
salesforce_opportunity_stage_changed: salesforceOpportunityStageChangedTrigger,
438445
salesforce_case_status_changed: salesforceCaseStatusChangedTrigger,
439446
salesforce_webhook: salesforceWebhookTrigger,
447+
servicenow_incident_created: servicenowIncidentCreatedTrigger,
448+
servicenow_incident_updated: servicenowIncidentUpdatedTrigger,
449+
servicenow_change_request_created: servicenowChangeRequestCreatedTrigger,
450+
servicenow_change_request_updated: servicenowChangeRequestUpdatedTrigger,
451+
servicenow_webhook: servicenowWebhookTrigger,
440452
stripe_webhook: stripeWebhookTrigger,
441453
telegram_webhook: telegramWebhookTrigger,
442454
typeform_webhook: typeformWebhookTrigger,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ServiceNowIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildChangeRequestOutputs,
5+
buildServiceNowExtraFields,
6+
servicenowSetupInstructions,
7+
servicenowTriggerOptions,
8+
} from '@/triggers/servicenow/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* ServiceNow Change Request Created Trigger
13+
*/
14+
export const servicenowChangeRequestCreatedTrigger: TriggerConfig = {
15+
id: 'servicenow_change_request_created',
16+
name: 'ServiceNow Change Request Created',
17+
provider: 'servicenow',
18+
description: 'Trigger workflow when a new change request is created in ServiceNow',
19+
version: '1.0.0',
20+
icon: ServiceNowIcon,
21+
22+
subBlocks: buildTriggerSubBlocks({
23+
triggerId: 'servicenow_change_request_created',
24+
triggerOptions: servicenowTriggerOptions,
25+
setupInstructions: servicenowSetupInstructions('Insert (record creation)'),
26+
extraFields: buildServiceNowExtraFields('servicenow_change_request_created'),
27+
}),
28+
29+
outputs: buildChangeRequestOutputs(),
30+
31+
webhook: {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
},
36+
},
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ServiceNowIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildChangeRequestOutputs,
5+
buildServiceNowExtraFields,
6+
servicenowSetupInstructions,
7+
servicenowTriggerOptions,
8+
} from '@/triggers/servicenow/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* ServiceNow Change Request Updated Trigger
13+
*/
14+
export const servicenowChangeRequestUpdatedTrigger: TriggerConfig = {
15+
id: 'servicenow_change_request_updated',
16+
name: 'ServiceNow Change Request Updated',
17+
provider: 'servicenow',
18+
description: 'Trigger workflow when a change request is updated in ServiceNow',
19+
version: '1.0.0',
20+
icon: ServiceNowIcon,
21+
22+
subBlocks: buildTriggerSubBlocks({
23+
triggerId: 'servicenow_change_request_updated',
24+
triggerOptions: servicenowTriggerOptions,
25+
setupInstructions: servicenowSetupInstructions('Update (record modification)'),
26+
extraFields: buildServiceNowExtraFields('servicenow_change_request_updated'),
27+
}),
28+
29+
outputs: buildChangeRequestOutputs(),
30+
31+
webhook: {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
},
36+
},
37+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ServiceNowIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildIncidentOutputs,
5+
buildServiceNowExtraFields,
6+
servicenowSetupInstructions,
7+
servicenowTriggerOptions,
8+
} from '@/triggers/servicenow/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* ServiceNow Incident Created Trigger
13+
*
14+
* Primary trigger — includes the dropdown for selecting trigger type.
15+
*/
16+
export const servicenowIncidentCreatedTrigger: TriggerConfig = {
17+
id: 'servicenow_incident_created',
18+
name: 'ServiceNow Incident Created',
19+
provider: 'servicenow',
20+
description: 'Trigger workflow when a new incident is created in ServiceNow',
21+
version: '1.0.0',
22+
icon: ServiceNowIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'servicenow_incident_created',
26+
triggerOptions: servicenowTriggerOptions,
27+
includeDropdown: true,
28+
setupInstructions: servicenowSetupInstructions('Insert (record creation)'),
29+
extraFields: buildServiceNowExtraFields('servicenow_incident_created'),
30+
}),
31+
32+
outputs: buildIncidentOutputs(),
33+
34+
webhook: {
35+
method: 'POST',
36+
headers: {
37+
'Content-Type': 'application/json',
38+
},
39+
},
40+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ServiceNowIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildIncidentOutputs,
5+
buildServiceNowExtraFields,
6+
servicenowSetupInstructions,
7+
servicenowTriggerOptions,
8+
} from '@/triggers/servicenow/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* ServiceNow Incident Updated Trigger
13+
*/
14+
export const servicenowIncidentUpdatedTrigger: TriggerConfig = {
15+
id: 'servicenow_incident_updated',
16+
name: 'ServiceNow Incident Updated',
17+
provider: 'servicenow',
18+
description: 'Trigger workflow when an incident is updated in ServiceNow',
19+
version: '1.0.0',
20+
icon: ServiceNowIcon,
21+
22+
subBlocks: buildTriggerSubBlocks({
23+
triggerId: 'servicenow_incident_updated',
24+
triggerOptions: servicenowTriggerOptions,
25+
setupInstructions: servicenowSetupInstructions('Update (record modification)'),
26+
extraFields: buildServiceNowExtraFields('servicenow_incident_updated'),
27+
}),
28+
29+
outputs: buildIncidentOutputs(),
30+
31+
webhook: {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
},
36+
},
37+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { servicenowChangeRequestCreatedTrigger } from './change_request_created'
2+
export { servicenowChangeRequestUpdatedTrigger } from './change_request_updated'
3+
export { servicenowIncidentCreatedTrigger } from './incident_created'
4+
export { servicenowIncidentUpdatedTrigger } from './incident_updated'
5+
export { servicenowWebhookTrigger } from './webhook'

0 commit comments

Comments
 (0)