skills/development/scheduled-jobs/SKILL.md
Comprehensive guide to creating and managing ServiceNow scheduled jobs - run frequencies, conditional execution, performance optimization, error handling, and debugging
npx skillsauth add happy-technologies-llc/happy-servicenow-skills scheduled-jobsInstall 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 creating and managing ServiceNow scheduled jobs for automated background processing:
sysauto_script) - Recurring jobs with cron-like schedulingsys_trigger) - One-time or conditional job executionsWhen to use: When you need to automate recurring tasks, batch processing, data cleanup, notifications, or any background operations that run on a schedule.
Who should use this: Developers and administrators who need to automate ServiceNow operations without user intervention.
admin (required for scheduled job creation and management)sysauto_script, sys_trigger, syslog tablesadmin/script-execution - Background script patternsadmin/batch-operations - Bulk record operations| Table | Purpose | Key Fields |
|-------|---------|------------|
| sysauto_script | Scheduled script executions (recurring) | name, script, run_type, run_time, conditional |
| sys_trigger | System triggers (one-time/system events) | name, script, next_action, trigger_type, state |
| syslog | Execution logs | message, level, source, sys_created_on |
| sys_script | Script includes (reusable logic) | name, script, active, api_name |
| Run Type | Value | Description |
|----------|-------|-------------|
| Run Once | on_demand | Manual execution only |
| Daily | daily | Runs every day at specified time |
| Weekly | weekly | Runs on specified day(s) of week |
| Monthly | monthly | Runs on specified day of month |
| Periodically | periodically | Runs at fixed intervals |
| On Demand | on_demand | Manual trigger only |
Create a scheduled job that runs daily.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Daily Incident Cleanup"
script: |
// Daily cleanup of resolved incidents older than 90 days
var cutoffDate = gs.daysAgo(90);
var gr = new GlideRecord('incident');
gr.addQuery('state', 'IN', '6,7,8'); // Resolved, Closed, Cancelled
gr.addQuery('sys_updated_on', '<', cutoffDate);
gr.setLimit(1000); // Process in batches
gr.query();
var archived = 0;
while (gr.next()) {
// Archive logic here
archived++;
}
gs.info('[Daily Incident Cleanup] Processed ' + archived + ' incidents');
active: true
run_type: daily
run_time: "02:00:00"
run_dayofweek: 1
Key Fields Explained:
run_type: daily - Runs every dayrun_time: "02:00:00" - 2:00 AM (use 24-hour format)run_dayofweek: 1 - Monday (1=Mon, 2=Tue, ..., 7=Sun) - used for weeklyCreate a job that runs every Monday at 6 AM.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Weekly SLA Report"
script: |
// Generate weekly SLA compliance report
gs.info('[Weekly SLA Report] Starting report generation');
var report = {
totalIncidents: 0,
slaBreaches: 0,
slaCompliance: 0
};
var gr = new GlideRecord('task_sla');
gr.addQuery('stage', 'IN', 'completed,cancelled');
gr.addQuery('sys_created_on', '>=', gs.daysAgo(7));
gr.query();
while (gr.next()) {
report.totalIncidents++;
if (gr.has_breached) {
report.slaBreaches++;
}
}
report.slaCompliance = report.totalIncidents > 0
? Math.round((1 - report.slaBreaches / report.totalIncidents) * 100)
: 100;
gs.info('[Weekly SLA Report] Results: ' + JSON.stringify(report));
active: true
run_type: weekly
run_time: "06:00:00"
run_dayofweek: 1
Create a job that runs on the 1st of each month.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Monthly User Audit"
script: |
// Monthly audit of inactive users
gs.info('[Monthly User Audit] Starting audit');
var cutoffDate = gs.daysAgo(90);
var inactiveUsers = [];
var gr = new GlideRecord('sys_user');
gr.addQuery('active', true);
gr.addQuery('last_login_time', '<', cutoffDate);
gr.addQuery('last_login_time', '!=', '');
gr.query();
while (gr.next()) {
inactiveUsers.push({
user_id: gr.user_name.toString(),
name: gr.name.toString(),
last_login: gr.last_login_time.toString()
});
}
gs.info('[Monthly User Audit] Found ' + inactiveUsers.length + ' inactive users');
// Optional: Send report email
if (inactiveUsers.length > 0) {
var email = new GlideEmailOutbound();
email.setSubject('Monthly Inactive User Report');
email.setFrom('[email protected]');
email.addAddress('[email protected]');
email.setBody('Found ' + inactiveUsers.length + ' users inactive for 90+ days.\n\n' +
JSON.stringify(inactiveUsers, null, 2));
email.send();
}
active: true
run_type: monthly
run_time: "03:00:00"
run_dayofmonth: 1
Create a job that runs every 15 minutes.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Integration Queue Processor"
script: |
// Process integration queue every 15 minutes
gs.info('[Queue Processor] Starting processing');
var gr = new GlideRecord('x_custom_integration_queue');
gr.addQuery('state', 'pending');
gr.addQuery('retry_count', '<', 3);
gr.orderBy('priority');
gr.orderBy('sys_created_on');
gr.setLimit(100); // Batch size
gr.query();
var processed = 0;
var failed = 0;
while (gr.next()) {
try {
// Process queue item
processQueueItem(gr);
gr.state = 'completed';
gr.update();
processed++;
} catch (e) {
gr.retry_count = gr.retry_count + 1;
gr.error_message = e.message;
gr.state = gr.retry_count >= 3 ? 'failed' : 'pending';
gr.update();
failed++;
gs.error('[Queue Processor] Error: ' + e.message);
}
}
gs.info('[Queue Processor] Completed. Processed=' + processed + ', Failed=' + failed);
function processQueueItem(record) {
// Your processing logic here
}
active: true
run_type: periodically
run_period: 900000
Note: run_period is in milliseconds (900000 = 15 minutes)
| Interval | Milliseconds | |----------|--------------| | 1 minute | 60000 | | 5 minutes | 300000 | | 15 minutes | 900000 | | 30 minutes | 1800000 | | 1 hour | 3600000 | | 4 hours | 14400000 | | 12 hours | 43200000 |
Create a job that only runs if there are records to process.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Process Pending Approvals"
script: |
// Process stale approval requests
gs.info('[Approval Processor] Starting');
var staleThreshold = gs.hoursAgo(24);
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('state', 'requested');
gr.addQuery('sys_created_on', '<', staleThreshold);
gr.query();
var escalated = 0;
while (gr.next()) {
// Escalate stale approvals
escalateApproval(gr);
escalated++;
}
gs.info('[Approval Processor] Escalated ' + escalated + ' approvals');
function escalateApproval(approval) {
// Send reminder notification
var email = new GlideEmailOutbound();
email.setSubject('Reminder: Approval Pending - ' + approval.document_id.getDisplayValue());
email.addAddress(approval.approver.email);
email.setBody('You have a pending approval request that requires your attention.\n\n' +
'Document: ' + approval.document_id.getDisplayValue() + '\n' +
'Requested: ' + approval.sys_created_on.getDisplayValue());
email.send();
}
active: true
run_type: daily
run_time: "09:00:00"
conditional: true
condition: |
// Only run if there are stale approvals
var staleThreshold = gs.hoursAgo(24);
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('state', 'requested');
gr.addQuery('sys_created_on', '<', staleThreshold);
gr.setLimit(1);
gr.query();
answer = gr.hasNext();
Key Fields:
conditional: true - Enables condition checkingcondition - Script that sets answer = true/falseCreate a job that only runs during business hours.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Business Hours Queue Monitor"
script: |
// Monitor queue during business hours
gs.info('[Queue Monitor] Checking queue status');
var gr = new GlideRecord('x_custom_queue');
gr.addQuery('state', 'pending');
gr.addQuery('priority', 1);
gr.query();
if (gr.getRowCount() > 10) {
// Alert on high queue depth
gs.eventQueue('queue.high_depth', gr, gr.getRowCount());
}
active: true
run_type: periodically
run_period: 600000
conditional: true
condition: |
// Only run between 8 AM and 6 PM, Monday-Friday
var now = new GlideDateTime();
var hour = parseInt(now.getLocalTime().getHourOfDayLocalTime());
var dayOfWeek = now.getDayOfWeekLocalTime();
// Monday=2, Friday=6 in GlideDateTime
var isWeekday = dayOfWeek >= 2 && dayOfWeek <= 6;
var isBusinessHours = hour >= 8 && hour < 18;
answer = isWeekday && isBusinessHours;
CRITICAL: Scheduled jobs must be optimized to avoid performance issues. The most common mistake is processing ALL records instead of filtering to only those needing work.
BAD Pattern (Full Table Scan):
// DON'T DO THIS - Scans entire table every time
var gr = new GlideRecord('incident');
gr.query();
while (gr.next()) {
if (gr.state == 1 && gr.priority == 1) {
// Process
}
}
GOOD Pattern (Filtered Query):
// DO THIS - Only retrieves records needing processing
var gr = new GlideRecord('incident');
gr.addQuery('state', 1); // New
gr.addQuery('priority', 1); // P1
gr.addQuery('processed', false); // Not yet processed
gr.setLimit(100); // Batch limit
gr.query();
while (gr.next()) {
// Process
}
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Batch Data Processor"
script: |
// Efficient batch processing pattern
var BATCH_SIZE = 100;
var MAX_BATCHES = 10;
var TABLE = 'incident';
var QUERY = 'state=1^priority=1^u_needs_processing=true';
gs.info('[Batch Processor] Starting');
var totalProcessed = 0;
var batchNum = 0;
while (batchNum < MAX_BATCHES) {
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.setLimit(BATCH_SIZE);
gr.query();
if (!gr.hasNext()) {
gs.info('[Batch Processor] No more records to process');
break;
}
var batchProcessed = 0;
while (gr.next()) {
try {
processRecord(gr);
gr.u_needs_processing = false;
gr.update();
batchProcessed++;
totalProcessed++;
} catch (e) {
gs.error('[Batch Processor] Error processing ' + gr.number + ': ' + e.message);
}
}
gs.info('[Batch Processor] Batch ' + (batchNum + 1) + ' complete: ' + batchProcessed + ' records');
batchNum++;
}
gs.info('[Batch Processor] Total processed: ' + totalProcessed);
function processRecord(record) {
// Your processing logic
}
active: true
run_type: periodically
run_period: 300000
BAD Pattern (Loads All Records):
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.query();
var count = gr.getRowCount(); // Loads all records first!
GOOD Pattern (Uses Aggregate):
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
var count = 0;
if (ga.next()) {
count = parseInt(ga.getAggregate('COUNT'));
}
Ensure your queries use indexed fields. Common indexed fields:
| Table | Indexed Fields |
|-------|---------------|
| incident | number, sys_id, active, state, priority, assigned_to, assignment_group |
| task | number, sys_id, active, state, assigned_to, assignment_group |
| sys_user | sys_id, user_name, email, active |
| sys_user_group | sys_id, name, active |
Verify Indexes:
Tool: SN-Query-Table
Parameters:
table_name: sys_index
query: table=incident
fields: name,columns,active
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Robust Data Sync"
script: |
// Robust scheduled job with comprehensive error handling
var JOB_NAME = 'Data Sync';
var startTime = new GlideDateTime();
var stats = {
processed: 0,
errors: 0,
skipped: 0,
errorDetails: []
};
gs.info('[' + JOB_NAME + '] Starting execution');
try {
// Main processing logic
var gr = new GlideRecord('x_custom_sync_queue');
gr.addQuery('state', 'pending');
gr.setLimit(100);
gr.query();
while (gr.next()) {
try {
if (!validateRecord(gr)) {
stats.skipped++;
continue;
}
syncRecord(gr);
gr.state = 'completed';
gr.update();
stats.processed++;
} catch (recordError) {
stats.errors++;
stats.errorDetails.push({
sys_id: gr.sys_id.toString(),
error: recordError.message
});
gr.state = 'error';
gr.error_message = recordError.message;
gr.update();
gs.error('[' + JOB_NAME + '] Record error: ' + gr.sys_id + ' - ' + recordError.message);
}
}
// Success summary
gs.info('[' + JOB_NAME + '] Summary: Processed=' + stats.processed +
', Errors=' + stats.errors + ', Skipped=' + stats.skipped);
} catch (fatalError) {
// Fatal error - entire job failed
gs.error('[' + JOB_NAME + '] FATAL ERROR: ' + fatalError.message);
gs.error('[' + JOB_NAME + '] Stack: ' + fatalError.stack);
// Send alert notification
sendAlertEmail(JOB_NAME, fatalError, stats);
} finally {
// Always log duration
var endTime = new GlideDateTime();
var duration = GlideDateTime.subtract(startTime, endTime).getNumericValue() / 1000;
gs.info('[' + JOB_NAME + '] Duration: ' + duration + ' seconds');
// Send error report if there were errors
if (stats.errors > 0) {
sendErrorReport(JOB_NAME, stats);
}
}
function validateRecord(record) {
return record.getValue('source_id') && record.getValue('target_table');
}
function syncRecord(record) {
// Your sync logic here
}
function sendAlertEmail(jobName, error, stats) {
var email = new GlideEmailOutbound();
email.setSubject('[ALERT] Scheduled Job Failed: ' + jobName);
email.setFrom('[email protected]');
email.addAddress('[email protected]');
email.setBody('The scheduled job "' + jobName + '" has failed.\n\n' +
'Error: ' + error.message + '\n\n' +
'Stats at failure:\n' + JSON.stringify(stats, null, 2));
email.send();
}
function sendErrorReport(jobName, stats) {
var email = new GlideEmailOutbound();
email.setSubject('[WARN] Scheduled Job Errors: ' + jobName);
email.setFrom('[email protected]');
email.addAddress('[email protected]');
email.setBody('The scheduled job "' + jobName + '" completed with errors.\n\n' +
'Summary:\n' +
'- Processed: ' + stats.processed + '\n' +
'- Errors: ' + stats.errors + '\n' +
'- Skipped: ' + stats.skipped + '\n\n' +
'Error Details:\n' + JSON.stringify(stats.errorDetails, null, 2));
email.send();
}
active: true
run_type: periodically
run_period: 900000
Use ServiceNow events for notifications instead of direct email.
Create Event Registration:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_register
data:
event_name: "scheduled.job.failed"
suffix: "alert"
script_action: true
description: "Scheduled job failure alert"
Use in Script:
// In your scheduled job script
try {
// Processing logic
} catch (e) {
gs.eventQueue('scheduled.job.failed', null, JOB_NAME, e.message);
}
Find Recent Executions:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: source=Scheduled Job^sys_created_on>javascript:gs.hoursAgo(24)
fields: message,level,sys_created_on
limit: 100
orderByDesc: sys_created_on
Find Specific Job Logs:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: messageLIKE[Daily Incident Cleanup]^sys_created_on>javascript:gs.hoursAgo(24)
fields: message,level,sys_created_on
limit: 50
Find Error Logs:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: level=2^source=Scheduled Job^sys_created_on>javascript:gs.hoursAgo(24)
fields: message,sys_created_on,source
limit: 50
List All Active Scheduled Jobs:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: active=true
fields: name,run_type,run_time,run_period,next_action,sys_updated_on
limit: 100
Check Specific Job:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: name=Daily Incident Cleanup
fields: name,active,run_type,run_time,next_action,script
Check System Triggers:
Tool: SN-Query-Table
Parameters:
table_name: sys_trigger
query: next_action>javascript:gs.nowDateTime()^state=0
fields: name,next_action,trigger_type,state
limit: 50
Check for Stuck Jobs:
Tool: SN-Query-Table
Parameters:
table_name: sys_trigger
query: state=1^sys_updated_on<javascript:gs.hoursAgo(1)
fields: name,state,started_at,sys_updated_on
Using SN-Execute-Background-Script:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Execute scheduled job on demand
var job = new GlideRecord('sysauto_script');
job.addQuery('name', 'Daily Incident Cleanup');
job.query();
if (job.next()) {
var sched = new ScheduleOnce();
sched.script = job.script;
sched.setName('Manual Run: ' + job.name);
sched.schedule();
gs.info('Job scheduled for immediate execution: ' + job.name);
}
description: Trigger scheduled job manually
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_trigger
data:
name: "One-Time Data Migration"
script: |
gs.info('[Migration] Starting one-time migration');
// Your migration logic here
gs.info('[Migration] Complete');
next_action: 2026-02-06 14:00:00
trigger_type: 0
state: 0
Trigger Type Values: | Value | Description | |-------|-------------| | 0 | Run once | | 1 | Run periodically | | 2 | On event |
For very long-running operations, chain multiple executions.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Large Data Migration"
script: |
// Self-scheduling pattern for large operations
var RECORDS_PER_RUN = 1000;
var PROPERTY_NAME = 'x_custom.migration.last_id';
var lastId = gs.getProperty(PROPERTY_NAME, '');
gs.info('[Migration] Starting from: ' + (lastId || 'beginning'));
var gr = new GlideRecord('x_custom_source');
if (lastId) {
gr.addQuery('sys_id', '>', lastId);
}
gr.orderBy('sys_id');
gr.setLimit(RECORDS_PER_RUN);
gr.query();
var processed = 0;
var newLastId = '';
while (gr.next()) {
migrateRecord(gr);
processed++;
newLastId = gr.sys_id.toString();
}
if (processed > 0) {
gs.setProperty(PROPERTY_NAME, newLastId);
gs.info('[Migration] Processed ' + processed + ' records. Last ID: ' + newLastId);
// Check if more records exist
var remaining = new GlideAggregate('x_custom_source');
remaining.addQuery('sys_id', '>', newLastId);
remaining.addAggregate('COUNT');
remaining.query();
if (remaining.next()) {
var count = parseInt(remaining.getAggregate('COUNT'));
gs.info('[Migration] ' + count + ' records remaining');
if (count > 0) {
// Schedule next run immediately
scheduleNextRun();
}
}
} else {
gs.info('[Migration] Complete! No more records to process.');
gs.setProperty(PROPERTY_NAME, ''); // Reset for future runs
}
function migrateRecord(record) {
// Migration logic
}
function scheduleNextRun() {
var trigger = new GlideRecord('sys_trigger');
trigger.initialize();
trigger.name = 'Migration Continuation - ' + gs.nowDateTime();
trigger.script = 'gs.include("MigrationScheduler");';
trigger.trigger_type = 0;
trigger.state = 0;
var nextAction = new GlideDateTime();
nextAction.addSeconds(10);
trigger.next_action = nextAction;
trigger.insert();
gs.info('[Migration] Scheduled next run in 10 seconds');
}
active: false
run_type: on_demand
Create jobs that depend on other jobs completing.
Using MCP:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Job Chain - Step 1: Extract"
script: |
// Step 1: Extract data
gs.info('[Extract] Starting data extraction');
// Your extraction logic
var extractedData = [];
// ... populate extractedData
// Store in temp table for next step
var temp = new GlideRecord('x_custom_job_data');
temp.initialize();
temp.job_name = 'DataSync';
temp.step = 'extract';
temp.data = JSON.stringify(extractedData);
temp.status = 'completed';
temp.insert();
// Trigger next step
var trigger = new GlideRecord('sys_trigger');
trigger.initialize();
trigger.name = 'Job Chain - Step 2: Transform';
trigger.script = 'new x_custom.JobChain().runStep2();';
trigger.trigger_type = 0;
trigger.state = 0;
var nextAction = new GlideDateTime();
nextAction.addSeconds(5);
trigger.next_action = nextAction;
trigger.insert();
gs.info('[Extract] Complete. Triggered Step 2.');
active: true
run_type: daily
run_time: "01:00:00"
Split large workloads across multiple scheduled triggers.
// Main job that distributes work
var WORKERS = 5;
var TABLE = 'incident';
var QUERY = 'active=true^needs_processing=true';
// Count total records
var ga = new GlideAggregate(TABLE);
ga.addEncodedQuery(QUERY);
ga.addAggregate('COUNT');
ga.query();
var totalRecords = 0;
if (ga.next()) {
totalRecords = parseInt(ga.getAggregate('COUNT'));
}
if (totalRecords == 0) {
gs.info('[Distributor] No records to process');
return;
}
var batchSize = Math.ceil(totalRecords / WORKERS);
gs.info('[Distributor] Distributing ' + totalRecords + ' records across ' + WORKERS + ' workers');
// Create worker triggers
for (var i = 0; i < WORKERS; i++) {
var trigger = new GlideRecord('sys_trigger');
trigger.initialize();
trigger.name = 'Worker ' + (i + 1) + ' of ' + WORKERS;
trigger.script = 'processWorkerBatch(' + i + ', ' + batchSize + ');';
trigger.trigger_type = 0;
trigger.state = 0;
trigger.next_action = gs.nowDateTime();
trigger.insert();
}
// Standard cleanup job template
var JOB_NAME = 'Data Cleanup';
var TABLE = 'your_table';
var QUERY = 'active=false^sys_updated_on<javascript:gs.daysAgo(90)';
var BATCH_SIZE = 500;
var DRY_RUN = true; // Set to false to actually delete
gs.info('[' + JOB_NAME + '] Starting');
var deleted = 0;
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.setLimit(BATCH_SIZE);
gr.query();
while (gr.next()) {
if (DRY_RUN) {
gs.info('[' + JOB_NAME + '] [DRY RUN] Would delete: ' + gr.sys_id);
} else {
gr.deleteRecord();
}
deleted++;
}
gs.info('[' + JOB_NAME + '] ' + (DRY_RUN ? '[DRY RUN] ' : '') + 'Deleted ' + deleted + ' records');
// Notification job template
var JOB_NAME = 'Overdue Task Reminder';
var TABLE = 'task';
var QUERY = 'active=true^due_date<javascript:gs.nowDateTime()^state!=3';
gs.info('[' + JOB_NAME + '] Starting');
var notified = 0;
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.query();
while (gr.next()) {
if (gr.assigned_to.email) {
var email = new GlideEmailOutbound();
email.setSubject('Overdue Task: ' + gr.number);
email.setFrom('[email protected]');
email.addAddress(gr.assigned_to.email);
email.setBody('Your task ' + gr.number + ' is overdue.\n\n' +
'Description: ' + gr.short_description + '\n' +
'Due Date: ' + gr.due_date.getDisplayValue());
email.send();
notified++;
}
}
gs.info('[' + JOB_NAME + '] Sent ' + notified + ' notifications');
// Data sync job template
var JOB_NAME = 'External System Sync';
var SOURCE_TABLE = 'x_custom_staging';
var TARGET_TABLE = 'cmdb_ci_server';
var BATCH_SIZE = 100;
var stats = { created: 0, updated: 0, errors: 0 };
gs.info('[' + JOB_NAME + '] Starting sync');
var gr = new GlideRecord(SOURCE_TABLE);
gr.addQuery('sync_status', 'pending');
gr.setLimit(BATCH_SIZE);
gr.query();
while (gr.next()) {
try {
var target = new GlideRecord(TARGET_TABLE);
target.addQuery('serial_number', gr.serial_number);
target.query();
if (target.next()) {
// Update existing
updateRecord(target, gr);
target.update();
stats.updated++;
} else {
// Create new
target.initialize();
updateRecord(target, gr);
target.insert();
stats.created++;
}
gr.sync_status = 'completed';
gr.sync_date = gs.nowDateTime();
gr.update();
} catch (e) {
gr.sync_status = 'error';
gr.sync_error = e.message;
gr.update();
stats.errors++;
gs.error('[' + JOB_NAME + '] Error: ' + e.message);
}
}
gs.info('[' + JOB_NAME + '] Summary: Created=' + stats.created +
', Updated=' + stats.updated + ', Errors=' + stats.errors);
function updateRecord(target, source) {
target.name = source.hostname;
target.serial_number = source.serial_number;
target.ip_address = source.ip_address;
// Add more field mappings
}
// Audit job template
var JOB_NAME = 'Security Audit';
var AUDIT_RESULTS = [];
gs.info('[' + JOB_NAME + '] Starting audit');
// Audit 1: Users with admin role but no MFA
var gr = new GlideRecord('sys_user_has_role');
gr.addQuery('role.name', 'admin');
gr.addQuery('user.active', true);
gr.addQuery('user.two_factor_auth', false);
gr.query();
while (gr.next()) {
AUDIT_RESULTS.push({
type: 'admin_no_mfa',
user: gr.user.user_name.toString(),
severity: 'high'
});
}
// Audit 2: Service accounts with interactive login
var serviceAccounts = new GlideRecord('sys_user');
serviceAccounts.addQuery('user_name', 'CONTAINS', 'svc_');
serviceAccounts.addQuery('locked_out', false);
serviceAccounts.addQuery('web_service_access_only', false);
serviceAccounts.query();
while (serviceAccounts.next()) {
AUDIT_RESULTS.push({
type: 'svc_interactive_login',
user: serviceAccounts.user_name.toString(),
severity: 'medium'
});
}
// Log results
gs.info('[' + JOB_NAME + '] Found ' + AUDIT_RESULTS.length + ' findings');
// Store audit results
if (AUDIT_RESULTS.length > 0) {
var auditRecord = new GlideRecord('x_custom_audit_log');
auditRecord.initialize();
auditRecord.audit_type = JOB_NAME;
auditRecord.findings = JSON.stringify(AUDIT_RESULTS);
auditRecord.finding_count = AUDIT_RESULTS.length;
auditRecord.insert();
}
| Operation | MCP Tool | Purpose | |-----------|----------|---------| | Create job | SN-Create-Record | Create sysauto_script record | | Update job | SN-Update-Record | Modify job settings | | Query jobs | SN-Query-Table | List and check job status | | View logs | SN-Query-Table | Query syslog for execution history | | Execute now | SN-Execute-Background-Script | Run job logic immediately | | Get schema | SN-Get-Table-Schema | Understand table structure |
Symptom: Scheduled job doesn't execute at expected time Causes:
active: false on jobSolution:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: name=Your Job Name
fields: active,run_type,run_time,next_action,conditional,condition
Symptom: Job fails with timeout error Causes:
Solution:
Symptom: Job runs but no gs.info() output visible Causes:
Solution:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: sys_created_on>javascript:gs.minutesAgo(30)^sourceLIKEScheduled
fields: message,level,source,sys_created_on
limit: 100
Symptom: Periodic job runs more often than expected Causes:
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: nameLIKEYour Job
fields: name,active,run_period
Symptom: Conditional job skipped every time Causes:
answer variable not setSolution: Test condition script directly:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Test your condition
var gr = new GlideRecord('your_table');
gr.addQuery('your_query');
gr.setLimit(1);
gr.query();
var answer = gr.hasNext();
gs.info('Condition result: ' + answer);
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Cleanup Old Attachments"
script: |
var JOB_NAME = 'Attachment Cleanup';
var DAYS_OLD = 365;
var BATCH_SIZE = 100;
gs.info('[' + JOB_NAME + '] Starting');
var cutoff = gs.daysAgo(DAYS_OLD);
var deleted = 0;
var gr = new GlideRecord('sys_attachment');
gr.addQuery('sys_created_on', '<', cutoff);
gr.addQuery('table_name', 'NOT IN', 'kb_knowledge,sys_user');
gr.setLimit(BATCH_SIZE);
gr.query();
while (gr.next()) {
gr.deleteRecord();
deleted++;
}
gs.info('[' + JOB_NAME + '] Deleted ' + deleted + ' attachments');
active: true
run_type: weekly
run_time: "03:00:00"
run_dayofweek: 7
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "External API Sync"
script: |
var JOB_NAME = 'API Sync';
var MAX_RETRIES = 3;
var BATCH_SIZE = 50;
gs.info('[' + JOB_NAME + '] Starting');
var gr = new GlideRecord('x_custom_api_queue');
gr.addQuery('status', 'pending');
gr.addQuery('retry_count', '<', MAX_RETRIES);
gr.orderBy('priority');
gr.setLimit(BATCH_SIZE);
gr.query();
var stats = { success: 0, retry: 0, failed: 0 };
while (gr.next()) {
try {
var result = callExternalAPI(gr);
gr.status = 'completed';
gr.response_data = result;
stats.success++;
} catch (e) {
gr.retry_count++;
gr.last_error = e.message;
if (gr.retry_count >= MAX_RETRIES) {
gr.status = 'failed';
stats.failed++;
} else {
gr.status = 'pending';
stats.retry++;
}
}
gr.update();
}
gs.info('[' + JOB_NAME + '] Results: ' + JSON.stringify(stats));
function callExternalAPI(record) {
var rest = new sn_ws.RESTMessageV2();
rest.setEndpoint('https://api.example.com/sync');
rest.setHttpMethod('POST');
rest.setRequestBody(JSON.stringify({
id: record.external_id.toString(),
data: record.payload.toString()
}));
var response = rest.execute();
if (response.getStatusCode() != 200) {
throw new Error('API error: ' + response.getStatusCode());
}
return response.getBody();
}
active: true
run_type: periodically
run_period: 300000
admin/script-execution - Background script patternsadmin/batch-operations - Bulk record operationsadmin/update-set-management - Capture jobs in update setsitsm/incident-lifecycle - Incident automationtesting
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