import type { SubBlockConfig } from '@/blocks/types' import type { TriggerOutput } from '@/triggers/types' import { normalizeToken } from '@/triggers/utils/tokens' /** * 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.', '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.', ] return instructions .map( (instruction, index) => `
${index === 0 ? instruction : `${index}. ${instruction}`}
` ) .join('') } /** * 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)', 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' }, } } /** * 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) */ 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' }, } }