skills/development/notifications/SKILL.md
Complete guide to email notification development including events, templates, mail scripts, and delivery troubleshooting
npx skillsauth add happy-technologies-llc/happy-servicenow-skills notificationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill covers the complete process of creating and managing ServiceNow email notifications:
When to use: When building automated notifications for record changes, approvals, SLA breaches, workflow activities, or custom business events.
admin, notification_admin, or itil (for viewing)admin/workflow-creation for workflow-triggered notificationssysevent_register (Event Registry)
|
+-- sysevent_email_action (Email Notifications)
|
+-- sysevent_email_template (Email Templates)
+-- sys_script_email (Mail Scripts)
+-- cmn_notif_message (Notification Messages)
| Type | Trigger | Use Case | |------|---------|----------| | Event-based | System/custom events | Standard approach - decoupled, reusable | | Direct | Record conditions | Quick setup - tightly coupled to conditions | | Workflow | Workflow activity | Part of workflow orchestration | | Script | GlideEmailOutbound | Programmatic - full control |
| Table | Purpose | |-------|---------| | sysevent_email_action | Email notification definitions | | sysevent_email_template | Email body templates | | sysevent_register | Event definitions | | sys_script_email | Mail scripts for recipients | | sys_email | Email log (sent/failed) | | cmn_notif_message | Notification message store | | cmn_notif_device | User notification devices |
Event-Based Notifications (Recommended):
incident.assigned, change.approval.rejected)Direct Notifications:
Before creating notifications, understand available events.
Query Event Registry:
Tool: SN-Query-Table
Parameters:
table_name: sysevent_register
query: table=incident
fields: sys_id,event_name,table,description,fired_by
limit: 50
Common System Events:
| Event | Table | Trigger | |-------|-------|---------| | incident.assigned | incident | Incident assigned to user/group | | incident.commented | incident | Comment added | | incident.resolved | incident | Incident resolved | | incident.reopened | incident | Incident reopened | | change.approval.rejected | change_request | Change approval rejected | | change.itil.approval.rejected | change_request | ITIL change rejected | | sc_request.approved | sc_request | Request approved | | sla.warning | task_sla | SLA warning threshold | | sla.breached | task_sla | SLA breached | | approval.inserted | sysapproval_approver | Approval record created | | task.assigned | task | Task assigned |
If no existing event matches your needs, create a custom event.
Create Event Registration:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_register
data:
event_name: "x_custom.incident.escalated"
table: "incident"
description: "Fired when incident is escalated to management"
fired_by: "Business Rule: Incident Escalation"
Fire Event from Business Rule:
// In business rule script
if (current.escalation.changes() && current.escalation > 0) {
gs.eventQueue('x_custom.incident.escalated', current, current.assigned_to, current.assignment_group);
}
Fire Event via Background Script:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var inc = new GlideRecord('incident');
inc.get('number', 'INC0010001');
if (inc.isValidRecord()) {
gs.eventQueue('x_custom.incident.escalated', inc, inc.assigned_to.toString(), inc.assignment_group.toString());
gs.info('Event fired for: ' + inc.number);
}
description: "Fire custom escalation event for testing"
Basic Event-Based Notification:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "Incident Escalation Alert"
event_name: "x_custom.incident.escalated"
sys_domain: "global"
collection: "incident"
active: true
# Subject
subject: "URGENT: Incident ${number} Escalated - ${short_description}"
# Recipients
send_to_event_creator: false
recipient_users: [user_sys_id] # Specific users
recipient_groups: [group_sys_id] # Groups
send_to_event_parm_1: true # gs.eventQueue parm1 (assigned_to)
send_to_event_parm_2: true # gs.eventQueue parm2 (assignment_group)
# Content
content_type: "text/html"
message_html: |
<html>
<body>
<h2>Incident Escalated</h2>
<p>The following incident has been escalated and requires immediate attention:</p>
<table border="1" cellpadding="5">
<tr><td><b>Number:</b></td><td>${number}</td></tr>
<tr><td><b>Priority:</b></td><td>${priority}</td></tr>
<tr><td><b>Description:</b></td><td>${short_description}</td></tr>
<tr><td><b>Assigned To:</b></td><td>${assigned_to}</td></tr>
<tr><td><b>Assignment Group:</b></td><td>${assignment_group}</td></tr>
<tr><td><b>Escalation Level:</b></td><td>${escalation}</td></tr>
</table>
<p><a href="${URI_REF}">View Incident</a></p>
</body>
</html>
Direct (Condition-Based) Notification:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "High Priority Incident Created"
# No event_name - uses conditions instead
sys_domain: "global"
collection: "incident"
action_insert: true
action_update: false
action_delete: false
condition: "priority=1^state=1" # P1, New state
active: true
subject: "P1 Incident Created: ${number}"
send_to_event_parm_1: true
content_type: "text/html"
message_html: |
<h2>Priority 1 Incident Created</h2>
<p>A new Priority 1 incident requires immediate attention.</p>
<p><b>Number:</b> ${number}</p>
<p><b>Description:</b> ${short_description}</p>
<p><a href="${URI_REF}">View Incident</a></p>
Recipient Options in sysevent_email_action:
| Field | Purpose |
|-------|---------|
| recipient_users | Specific user sys_ids (comma-separated) |
| recipient_groups | Group sys_ids - emails all members |
| send_to_event_creator | User who triggered event |
| send_to_event_parm_1 | First parameter from gs.eventQueue |
| send_to_event_parm_2 | Second parameter from gs.eventQueue |
| recipient_fields | Record field containing user reference |
| send_self | Send to user who made change |
| exclude_delegates | Skip delegate users |
| email_template | Reference to reusable template |
Field-Based Recipients:
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
recipient_fields: "assigned_to,opened_by,caller_id" # Comma-separated fields
Group Recipients:
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
recipient_groups: [group1_sys_id],[group2_sys_id]
Mail scripts allow complex recipient logic beyond simple field references.
Create Mail Script:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_email
data:
name: "Get On-Call Manager"
description: "Returns the on-call manager for the assignment group"
script: |
// Mail script to find on-call manager
// 'current' is the record, 'email' is the GlideEmailOutbound object
(function runMailScript(current, template, email, email_action, event) {
// Get on-call rotation for assignment group
var oncall = new GlideRecord('on_call_rotation');
oncall.addQuery('group', current.assignment_group);
oncall.addQuery('active', true);
oncall.query();
if (oncall.next()) {
// Add on-call user as recipient
var user = new GlideRecord('sys_user');
user.get(oncall.user);
if (user.isValidRecord() && user.email) {
email.addAddress('to', user.email, user.name);
gs.info('Mail script: Added recipient ' + user.email);
}
}
// Optionally add escalation manager
if (current.escalation > 1) {
var manager = current.assignment_group.manager;
if (manager && manager.email) {
email.addAddress('cc', manager.email, manager.name);
}
}
})(current, template, email, email_action, event);
Associate Mail Script with Notification:
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
sys_script_mail: [mail_script_sys_id]
Advanced Mail Script - Manager Hierarchy:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_email
data:
name: "Get Manager Chain"
description: "Adds manager hierarchy up to specified levels"
script: |
(function runMailScript(current, template, email, email_action, event) {
var MAX_LEVELS = 3;
var userId = current.assigned_to.toString();
for (var level = 0; level < MAX_LEVELS && userId; level++) {
var user = new GlideRecord('sys_user');
user.get(userId);
if (user.isValidRecord() && user.manager) {
var manager = new GlideRecord('sys_user');
manager.get(user.manager);
if (manager.isValidRecord() && manager.email) {
email.addAddress('cc', manager.email, manager.name);
userId = manager.sys_id.toString();
} else {
break;
}
} else {
break;
}
}
})(current, template, email, email_action, event);
Email templates allow reusable HTML/text formatting.
Create Email Template:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_template
data:
name: "Incident Alert Template"
description: "Standard template for incident notifications"
subject: "${number}: ${short_description}"
message_html: |
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.header { background-color: #1a73e8; color: white; padding: 20px; }
.content { padding: 20px; }
.details { border-collapse: collapse; width: 100%; }
.details td { border: 1px solid #ddd; padding: 10px; }
.label { background-color: #f5f5f5; font-weight: bold; width: 30%; }
.footer { background-color: #f5f5f5; padding: 15px; font-size: 12px; }
.button { background-color: #1a73e8; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; }
</style>
</head>
<body>
<div class="header">
<h1>Incident Alert</h1>
</div>
<div class="content">
<p>An incident requires your attention:</p>
<table class="details">
<tr>
<td class="label">Number</td>
<td>${number}</td>
</tr>
<tr>
<td class="label">Priority</td>
<td>${priority}</td>
</tr>
<tr>
<td class="label">State</td>
<td>${state}</td>
</tr>
<tr>
<td class="label">Description</td>
<td>${short_description}</td>
</tr>
<tr>
<td class="label">Assigned To</td>
<td>${assigned_to}</td>
</tr>
<tr>
<td class="label">Assignment Group</td>
<td>${assignment_group}</td>
</tr>
<tr>
<td class="label">Opened</td>
<td>${opened_at}</td>
</tr>
</table>
<p style="margin-top: 20px;">
<a href="${URI_REF}" class="button">View Incident</a>
</p>
</div>
<div class="footer">
<p>This is an automated notification from ServiceNow.</p>
<p>Do not reply to this email.</p>
</div>
</body>
</html>
message: |
Incident Alert
Number: ${number}
Priority: ${priority}
State: ${state}
Description: ${short_description}
Assigned To: ${assigned_to}
Assignment Group: ${assignment_group}
View Incident: ${URI_REF}
This is an automated notification.
collection: "incident"
Associate Template with Notification:
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
email_template: [template_sys_id]
# Clear inline content if using template
message_html: ""
message: ""
Standard Field Variables:
${field_name} - Display value of field
${field_name.name} - Reference field's name
${field_name.email} - Reference field's email
${field_name.sys_id} - Reference field's sys_id
${number} - Incident number
${short_description} - Short description
${priority} - Priority display value
${state} - State display value
Special Variables:
${URI} - Record URI path only
${URI_REF} - Full URL to record
${mailto:email} - Mailto link
${event.parm1} - First event parameter
${event.parm2} - Second event parameter
${current} - Current record reference
${gs.getProperty('name')} - System property value
Conditional Content (Mail Script Required):
// In notification body script or template script
if (current.priority == 1) {
template.print('<span style="color: red; font-weight: bold;">CRITICAL</span>');
} else {
template.print('<span>' + current.priority.getDisplayValue() + '</span>');
}
Iterating Over Related Records:
// Show related tasks in notification
template.print('<h3>Related Tasks:</h3>');
template.print('<ul>');
var tasks = new GlideRecord('sc_task');
tasks.addQuery('request_item', current.sys_id);
tasks.query();
while (tasks.next()) {
template.print('<li>' + tasks.number + ' - ' + tasks.short_description + '</li>');
}
template.print('</ul>');
Include Record Attachments:
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
include_attachments: true
Programmatic Attachment via Mail Script:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_email
data:
name: "Attach Report PDF"
description: "Attaches a generated report to the email"
script: |
(function runMailScript(current, template, email, email_action, event) {
// Get attachment from related record
var attach = new GlideRecord('sys_attachment');
attach.addQuery('table_name', 'incident');
attach.addQuery('table_sys_id', current.sys_id);
attach.addQuery('content_type', 'CONTAINS', 'pdf');
attach.query();
while (attach.next()) {
// Add each PDF attachment
var gsa = new GlideSysAttachment();
var content = gsa.getContentBase64(attach);
email.addAttachment(attach.file_name, attach.content_type, content);
}
})(current, template, email, email_action, event);
SMS notifications require the SMS plugin and carrier configuration.
Check SMS Capability:
Tool: SN-Query-Table
Parameters:
table_name: sys_plugins
query: name=com.snc.sms
fields: sys_id,name,active
Create SMS Notification:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "P1 Incident SMS Alert"
event_name: "incident.assigned"
sys_domain: "global"
collection: "incident"
condition: "priority=1"
active: true
# SMS specific
sms_alternate: true # Enable SMS option
subject: "P1: ${number}" # SMS subject (carrier dependent)
message: "P1 Incident ${number}: ${short_description}. Priority: ${priority}. View: ${URI_REF}"
content_type: "text/plain"
# Recipients
send_to_event_parm_1: true
Configure SMS for User:
Tool: SN-Query-Table
Parameters:
table_name: cmn_notif_device
query: user=[user_sys_id]^type=SMS
fields: sys_id,phone_number,carrier,active
Push notifications require the mobile plugin and device registration.
Create Push Notification:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "Approval Required Push"
event_name: "approval.inserted"
collection: "sysapproval_approver"
active: true
# Push specific
push_message_only: true # Send push only, no email
subject: "Approval Required"
message: "You have a new approval request: ${document_id.short_description}"
# Recipients
recipient_fields: "approver"
Query User's Push Devices:
Tool: SN-Query-Table
Parameters:
table_name: cmn_notif_device
query: user=[user_sys_id]^type=Push
fields: sys_id,device_name,active,last_registration
Advanced Condition Examples:
| Condition | Purpose |
|-----------|---------|
| priority=1^state!=6 | P1, not resolved |
| active=true^assigned_toISEMPTY | Active, unassigned |
| sys_created_onONToday@javascript:gs.beginningOfToday()@javascript:gs.endOfToday() | Created today |
| priority.changes()=true | Priority field changed |
| state.changesFrom(1)^state.changesTo(2) | State changed from New to In Progress |
Condition Script (Advanced):
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
advanced_condition: |
// Return true to send notification, false to skip
function advancedCondition() {
// Don't notify if CI is in maintenance window
if (current.cmdb_ci && current.cmdb_ci.maintenance) {
return false;
}
// Only notify during business hours
var now = new GlideDateTime();
var hour = now.getLocalTime().getHourOfDayLocalTime();
if (hour < 8 || hour > 18) {
return false; // Outside business hours
}
// Check if user has opted out
var prefs = new GlideRecord('sys_user_preference');
prefs.addQuery('user', current.assigned_to);
prefs.addQuery('name', 'notification.incident.optout');
prefs.addQuery('value', 'true');
prefs.query();
if (prefs.next()) {
return false;
}
return true;
}
advancedCondition();
Control notification frequency and importance.
Set Notification Weight:
Tool: SN-Update-Record
Parameters:
table_name: sysevent_email_action
sys_id: [notification_sys_id]
data:
weight: 10 # Higher weight = higher priority
digest: false # true = batch into digest
digest_interval: "" # hourly, daily if digest=true
omit_watermark: false # Watermark prevents duplicates
Digest Configuration:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "Daily Incident Summary"
event_name: "incident.created"
collection: "incident"
active: true
digest: true
digest_interval: "daily"
digest_template: [digest_template_sys_id]
subject: "Daily Incident Summary - ${digest.count} incidents"
Query Recent Emails:
Tool: SN-Query-Table
Parameters:
table_name: sys_email
query: type=send-ready^ORtype=sent^ORtype=send-error^sys_created_onONToday@javascript:gs.beginningOfToday()@javascript:gs.endOfToday()
fields: sys_id,type,recipients,subject,sys_created_on,error,mailbox
limit: 50
orderBy: sys_created_on
orderByDesc: true
Email Status Types:
| Type | Meaning | |------|---------| | send-ready | Queued for sending | | sent | Successfully sent | | send-error | Send failed | | received | Inbound email | | received-error | Inbound processing error | | draft | Pending approval | | ignored | Skipped by rule |
Find Failed Emails:
Tool: SN-Query-Table
Parameters:
table_name: sys_email
query: type=send-error^sys_created_onONLast 7 days@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)
fields: sys_id,recipients,subject,error,sys_created_on
limit: 100
Query Event Queue:
Tool: SN-Query-Table
Parameters:
table_name: sysevent
query: name=x_custom.incident.escalated^sys_created_onONToday@javascript:gs.beginningOfToday()@javascript:gs.endOfToday()
fields: sys_id,name,instance,parm1,parm2,state,sys_created_on
limit: 50
Event States:
| State | Meaning | |-------|---------| | ready | Queued for processing | | processed | Successfully processed | | error | Processing failed |
Check Notification Is Active:
Tool: SN-Query-Table
Parameters:
table_name: sysevent_email_action
query: sys_id=[notification_sys_id]
fields: name,active,event_name,condition,recipient_users,recipient_groups,recipient_fields
Verify Event Registration:
Tool: SN-Query-Table
Parameters:
table_name: sysevent_register
query: event_name=[your_event_name]
fields: sys_id,event_name,table,description
Fire Test Event:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Test notification by firing event
var inc = new GlideRecord('incident');
inc.addQuery('active', true);
inc.setLimit(1);
inc.query();
if (inc.next()) {
gs.eventQueue('x_custom.incident.escalated', inc, inc.assigned_to.toString(), inc.assignment_group.toString());
gs.info('Test event fired for: ' + inc.number);
}
description: "Fire test notification event"
Check Email Generation:
Tool: SN-Query-Table
Parameters:
table_name: sys_email
query: recipients=[test_email]^ORsubjectLIKE[incident_number]^sys_created_onONLast hour@javascript:gs.hoursAgoStart(1)@javascript:gs.hoursAgoEnd(0)
fields: sys_id,type,recipients,subject,body,error
Symptom: No email generated after record change Causes:
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sysevent_email_action
query: collection=incident^active=true
fields: name,event_name,condition,active,recipient_users,recipient_groups
Symptom: Email shows as sent but users don't receive Causes:
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sys_user
query: sys_id=[user_sys_id]
fields: email,notification,active
Check user notification preferences:
Tool: SN-Query-Table
Parameters:
table_name: cmn_notif_message
query: user=[user_sys_id]
fields: notification,device,state
Symptom: Emails fail with error status Causes:
Solution: Check the error field in sys_email record for specific error message.
Symptom: Email shows ${field_name} literally instead of value
Causes:
Solution: Verify field exists:
Tool: SN-Get-Table-Schema
Parameters:
table_name: incident
| Tool | Purpose |
|------|---------|
| SN-Query-Table | Query notifications, events, email logs |
| SN-Create-Record | Create notifications, templates, mail scripts |
| SN-Update-Record | Modify notification configuration |
| SN-Get-Table-Schema | Verify available fields for variables |
| SN-Execute-Background-Script | Fire test events, debug |
| Endpoint | Method | Purpose |
|----------|--------|---------|
| /api/now/table/sysevent_email_action | GET/POST | Query/create notifications |
| /api/now/table/sysevent_email_template | GET/POST | Query/create templates |
| /api/now/table/sys_script_email | POST | Create mail scripts |
| /api/now/table/sys_email | GET | Check email log |
| /api/now/table/sysevent_register | GET/POST | Manage events |
# 1. Register custom event
Tool: SN-Create-Record
Parameters:
table_name: sysevent_register
data:
event_name: "incident.p1.created"
table: "incident"
description: "Fired when Priority 1 incident is created"
# 2. Create notification
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "P1 Incident Alert - All Hands"
event_name: "incident.p1.created"
collection: "incident"
active: true
weight: 100
subject: "[URGENT] P1 Incident: ${number} - ${short_description}"
content_type: "text/html"
message_html: |
<h1 style="color: #d32f2f;">Priority 1 Incident Created</h1>
<p>A critical incident requires immediate attention.</p>
<table>
<tr><td><b>Number:</b></td><td>${number}</td></tr>
<tr><td><b>Description:</b></td><td>${short_description}</td></tr>
<tr><td><b>Caller:</b></td><td>${caller_id}</td></tr>
<tr><td><b>Assignment Group:</b></td><td>${assignment_group}</td></tr>
</table>
<p><a href="${URI_REF}">View Incident Now</a></p>
recipient_groups: [incident_managers_group_sys_id]
send_to_event_parm_1: true
send_to_event_parm_2: true
# 3. Create business rule to fire event
# (Create via UI or background script)
# 1. Create mail script for manager chain
Tool: SN-Create-Record
Parameters:
table_name: sys_script_email
data:
name: "CC Manager Chain"
script: |
(function runMailScript(current, template, email, email_action, event) {
// Get requested_for user
var reqFor = current.document_id.requested_for;
if (reqFor) {
var user = new GlideRecord('sys_user');
user.get(reqFor);
// Add manager as CC
if (user.manager && user.manager.email) {
email.addAddress('cc', user.manager.email.toString(), user.manager.name.toString());
}
}
})(current, template, email, email_action, event);
# 2. Create notification with mail script
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "Approval Request - With Manager CC"
event_name: "approval.inserted"
collection: "sysapproval_approver"
active: true
subject: "Approval Required: ${document_id.number}"
recipient_fields: "approver"
sys_script_mail: [mail_script_sys_id]
message_html: |
<h2>Approval Request</h2>
<p>You have a pending approval request.</p>
<p><b>Request:</b> ${document_id.number}</p>
<p><b>Requested By:</b> ${document_id.requested_for}</p>
<p><a href="${URI_REF}">Approve/Reject</a></p>
Tool: SN-Create-Record
Parameters:
table_name: sysevent_email_action
data:
name: "SLA Warning - 75% Elapsed"
event_name: "sla.warning"
collection: "task_sla"
active: true
condition: "percentage>=75^percentage<100"
subject: "SLA Warning: ${task.number} at ${percentage}%"
recipient_fields: "task.assigned_to"
content_type: "text/html"
message_html: |
<h2 style="color: #ff9800;">SLA Warning</h2>
<p>The following task is approaching SLA breach:</p>
<table>
<tr><td><b>Task:</b></td><td>${task.number}</td></tr>
<tr><td><b>SLA:</b></td><td>${sla.name}</td></tr>
<tr><td><b>Percentage:</b></td><td>${percentage}%</td></tr>
<tr><td><b>Time Remaining:</b></td><td>${business_time_left}</td></tr>
</table>
<p><a href="${task.URI_REF}">View Task</a></p>
admin/workflow-creation - Workflow-triggered notificationscatalog/approval-workflows - Approval notificationsitsm/incident-lifecycle - Incident notification triggersadmin/script-execution - Testing notifications via scriptstesting
Manage supplier onboarding, qualification, performance monitoring, and offboarding with auditable lifecycle controls
tools
Identify emerging risks, prioritize intake signals, and route candidates into formal GRC risk assessment workflows
documentation
Screen inbound documents for completeness, policy risk, and routing readiness before extraction or case workflows
testing
Generate concise task summaries with status, timeline, blockers, SLA risk, and recommended next actions